diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props
index faa6bae454a1..c0150982d16f 100644
--- a/dotnet/Directory.Packages.props
+++ b/dotnet/Directory.Packages.props
@@ -5,15 +5,15 @@
true
+
+
+
-
-
-
@@ -57,8 +57,8 @@
-
+
@@ -89,6 +89,7 @@
+
@@ -96,18 +97,18 @@
-
+
-
+
+
+
@@ -123,6 +124,7 @@
+
diff --git a/dotnet/SK-dotnet.sln b/dotnet/SK-dotnet.sln
index b7202e21a1b1..acf61a114486 100644
--- a/dotnet/SK-dotnet.sln
+++ b/dotnet/SK-dotnet.sln
@@ -533,6 +533,20 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ProcessWithCloudEvents.Proc
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ProcessWithCloudEvents.Grpc", "samples\Demos\ProcessWithCloudEvents\ProcessWithCloudEvents.Grpc\ProcessWithCloudEvents.Grpc.csproj", "{08D84994-794A-760F-95FD-4EFA8998A16D}"
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Runtime", "Runtime", "{A70ED5A7-F8E1-4A57-9455-3C05989542DA}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Runtime.Abstractions", "src\Agents\Runtime\Abstractions\Runtime.Abstractions.csproj", "{B9C86C5D-EB4C-8A16-E567-27025AC59A28}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Runtime.Abstractions.Tests", "src\Agents\Runtime\Abstractions.Tests\Runtime.Abstractions.Tests.csproj", "{BB74EEE2-F048-A1A4-F53E-2B384A6F8BC4}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Runtime.Core", "src\Agents\Runtime\Core\Runtime.Core.csproj", "{19DC60E6-AD08-4BCB-A4DF-B80E0941B458}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Runtime.Core.Tests", "src\Agents\Runtime\Core.Tests\Runtime.Core.Tests.csproj", "{A4F05541-7D23-A5A9-033D-382F1E13D0FE}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Runtime.InProcess", "src\Agents\Runtime\InProcess\Runtime.InProcess.csproj", "{CCC909E4-5269-A31E-0BFD-4863B4B29BBB}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Runtime.InProcess.Tests", "src\Agents\Runtime\InProcess.Tests\Runtime.InProcess.Tests.csproj", "{DA6B4ED4-ED0B-D25C-889C-9F940E714891}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -1458,6 +1472,42 @@ Global
{08D84994-794A-760F-95FD-4EFA8998A16D}.Publish|Any CPU.Build.0 = Release|Any CPU
{08D84994-794A-760F-95FD-4EFA8998A16D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{08D84994-794A-760F-95FD-4EFA8998A16D}.Release|Any CPU.Build.0 = Release|Any CPU
+ {B9C86C5D-EB4C-8A16-E567-27025AC59A28}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {B9C86C5D-EB4C-8A16-E567-27025AC59A28}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {B9C86C5D-EB4C-8A16-E567-27025AC59A28}.Publish|Any CPU.ActiveCfg = Release|Any CPU
+ {B9C86C5D-EB4C-8A16-E567-27025AC59A28}.Publish|Any CPU.Build.0 = Release|Any CPU
+ {B9C86C5D-EB4C-8A16-E567-27025AC59A28}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {B9C86C5D-EB4C-8A16-E567-27025AC59A28}.Release|Any CPU.Build.0 = Release|Any CPU
+ {BB74EEE2-F048-A1A4-F53E-2B384A6F8BC4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {BB74EEE2-F048-A1A4-F53E-2B384A6F8BC4}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {BB74EEE2-F048-A1A4-F53E-2B384A6F8BC4}.Publish|Any CPU.ActiveCfg = Release|Any CPU
+ {BB74EEE2-F048-A1A4-F53E-2B384A6F8BC4}.Publish|Any CPU.Build.0 = Release|Any CPU
+ {BB74EEE2-F048-A1A4-F53E-2B384A6F8BC4}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {BB74EEE2-F048-A1A4-F53E-2B384A6F8BC4}.Release|Any CPU.Build.0 = Release|Any CPU
+ {19DC60E6-AD08-4BCB-A4DF-B80E0941B458}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {19DC60E6-AD08-4BCB-A4DF-B80E0941B458}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {19DC60E6-AD08-4BCB-A4DF-B80E0941B458}.Publish|Any CPU.ActiveCfg = Release|Any CPU
+ {19DC60E6-AD08-4BCB-A4DF-B80E0941B458}.Publish|Any CPU.Build.0 = Release|Any CPU
+ {19DC60E6-AD08-4BCB-A4DF-B80E0941B458}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {19DC60E6-AD08-4BCB-A4DF-B80E0941B458}.Release|Any CPU.Build.0 = Release|Any CPU
+ {A4F05541-7D23-A5A9-033D-382F1E13D0FE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A4F05541-7D23-A5A9-033D-382F1E13D0FE}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A4F05541-7D23-A5A9-033D-382F1E13D0FE}.Publish|Any CPU.ActiveCfg = Release|Any CPU
+ {A4F05541-7D23-A5A9-033D-382F1E13D0FE}.Publish|Any CPU.Build.0 = Release|Any CPU
+ {A4F05541-7D23-A5A9-033D-382F1E13D0FE}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {A4F05541-7D23-A5A9-033D-382F1E13D0FE}.Release|Any CPU.Build.0 = Release|Any CPU
+ {CCC909E4-5269-A31E-0BFD-4863B4B29BBB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {CCC909E4-5269-A31E-0BFD-4863B4B29BBB}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {CCC909E4-5269-A31E-0BFD-4863B4B29BBB}.Publish|Any CPU.ActiveCfg = Release|Any CPU
+ {CCC909E4-5269-A31E-0BFD-4863B4B29BBB}.Publish|Any CPU.Build.0 = Release|Any CPU
+ {CCC909E4-5269-A31E-0BFD-4863B4B29BBB}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {CCC909E4-5269-A31E-0BFD-4863B4B29BBB}.Release|Any CPU.Build.0 = Release|Any CPU
+ {DA6B4ED4-ED0B-D25C-889C-9F940E714891}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {DA6B4ED4-ED0B-D25C-889C-9F940E714891}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {DA6B4ED4-ED0B-D25C-889C-9F940E714891}.Publish|Any CPU.ActiveCfg = Release|Any CPU
+ {DA6B4ED4-ED0B-D25C-889C-9F940E714891}.Publish|Any CPU.Build.0 = Release|Any CPU
+ {DA6B4ED4-ED0B-D25C-889C-9F940E714891}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {DA6B4ED4-ED0B-D25C-889C-9F940E714891}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -1656,6 +1706,13 @@ Global
{7C092DD9-9985-4D18-A817-15317D984149} = {5D4C0700-BBB5-418F-A7B2-F392B9A18263}
{31F6608A-FD36-F529-A5FC-C954A0B5E29E} = {7C092DD9-9985-4D18-A817-15317D984149}
{08D84994-794A-760F-95FD-4EFA8998A16D} = {7C092DD9-9985-4D18-A817-15317D984149}
+ {A70ED5A7-F8E1-4A57-9455-3C05989542DA} = {6823CD5E-2ABE-41EB-B865-F86EC13F0CF9}
+ {B9C86C5D-EB4C-8A16-E567-27025AC59A28} = {A70ED5A7-F8E1-4A57-9455-3C05989542DA}
+ {BB74EEE2-F048-A1A4-F53E-2B384A6F8BC4} = {A70ED5A7-F8E1-4A57-9455-3C05989542DA}
+ {19DC60E6-AD08-4BCB-A4DF-B80E0941B458} = {A70ED5A7-F8E1-4A57-9455-3C05989542DA}
+ {A4F05541-7D23-A5A9-033D-382F1E13D0FE} = {A70ED5A7-F8E1-4A57-9455-3C05989542DA}
+ {CCC909E4-5269-A31E-0BFD-4863B4B29BBB} = {A70ED5A7-F8E1-4A57-9455-3C05989542DA}
+ {DA6B4ED4-ED0B-D25C-889C-9F940E714891} = {A70ED5A7-F8E1-4A57-9455-3C05989542DA}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {FBDC56A3-86AD-4323-AA0F-201E59123B83}
diff --git a/dotnet/src/Agents/AzureAI/AzureAIAgent.cs b/dotnet/src/Agents/AzureAI/AzureAIAgent.cs
index 557299ae044e..440dd0c74b65 100644
--- a/dotnet/src/Agents/AzureAI/AzureAIAgent.cs
+++ b/dotnet/src/Agents/AzureAI/AzureAIAgent.cs
@@ -93,7 +93,7 @@ public AzureAIAgent(
}
///
- /// %%%
+ /// The associated client.
///
public AgentsClient Client { get; }
diff --git a/dotnet/src/Agents/Runtime/Abstractions.Tests/AgentIdTests.cs b/dotnet/src/Agents/Runtime/Abstractions.Tests/AgentIdTests.cs
new file mode 100644
index 000000000000..d0f338b62bcf
--- /dev/null
+++ b/dotnet/src/Agents/Runtime/Abstractions.Tests/AgentIdTests.cs
@@ -0,0 +1,122 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using FluentAssertions;
+using Xunit;
+
+namespace Microsoft.SemanticKernel.Agents.Runtime.Abstractions.Tests;
+
+[Trait("Category", "Unit")]
+public class AgentIdTests()
+{
+ [Theory]
+ [InlineData(null)]
+ [InlineData("")]
+ [InlineData(" ")]
+ [InlineData("invalid\u007Fkey")] // DEL character (127) is outside ASCII 32-126 range
+ [InlineData("invalid\u0000key")] // NULL character is outside ASCII 32-126 range
+ [InlineData("invalid\u0010key")] // Control character is outside ASCII 32-126 range
+ [InlineData("InvalidKey💀")] // Control character is outside ASCII 32-126 range
+ public void AgentIdShouldThrowArgumentExceptionWithInvalidKey(string? invalidKey)
+ {
+ // Act & Assert
+ ArgumentException exception = Assert.Throws(() => new AgentId("validType", invalidKey!));
+ Assert.Contains("Invalid AgentId key", exception.Message);
+ }
+
+ [Fact]
+ public void AgentIdShouldInitializeCorrectlyTest()
+ {
+ AgentId agentId = new("TestType", "TestKey");
+
+ agentId.Type.Should().Be("TestType");
+ agentId.Key.Should().Be("TestKey");
+ }
+
+ [Fact]
+ public void AgentIdShouldConvertFromTupleTest()
+ {
+ (string, string) agentTuple = ("TupleType", "TupleKey");
+ AgentId agentId = new(agentTuple);
+
+ agentId.Type.Should().Be("TupleType");
+ agentId.Key.Should().Be("TupleKey");
+ }
+
+ [Fact]
+ public void AgentIdShouldConvertFromAgentType()
+ {
+ AgentType agentType = "TestType";
+ AgentId agentId = new(agentType, "TestKey");
+
+ agentId.Type.Should().Be("TestType");
+ agentId.Key.Should().Be("TestKey");
+ }
+
+ [Fact]
+ public void AgentIdShouldParseFromStringTest()
+ {
+ AgentId agentId = AgentId.FromStr("ParsedType/ParsedKey");
+
+ agentId.Type.Should().Be("ParsedType");
+ agentId.Key.Should().Be("ParsedKey");
+ }
+
+ [Fact]
+ public void AgentIdShouldCompareEqualityCorrectlyTest()
+ {
+ AgentId agentId1 = new("SameType", "SameKey");
+ AgentId agentId2 = new("SameType", "SameKey");
+ AgentId agentId3 = new("DifferentType", "DifferentKey");
+
+ agentId1.Should().Be(agentId2);
+ agentId1.Should().NotBe(agentId3);
+ (agentId1 == agentId2).Should().BeTrue();
+ (agentId1 != agentId3).Should().BeTrue();
+ }
+
+ [Fact]
+ public void AgentIdShouldGenerateCorrectHashCodeTest()
+ {
+ AgentId agentId1 = new("HashType", "HashKey");
+ AgentId agentId2 = new("HashType", "HashKey");
+ AgentId agentId3 = new("DifferentType", "DifferentKey");
+
+ agentId1.GetHashCode().Should().Be(agentId2.GetHashCode());
+ agentId1.GetHashCode().Should().NotBe(agentId3.GetHashCode());
+ }
+
+ [Fact]
+ public void AgentIdShouldConvertExplicitlyFromStringTest()
+ {
+ AgentId agentId = (AgentId)"ConvertedType/ConvertedKey";
+
+ agentId.Type.Should().Be("ConvertedType");
+ agentId.Key.Should().Be("ConvertedKey");
+ }
+
+ [Fact]
+ public void AgentIdShouldReturnCorrectToStringTest()
+ {
+ AgentId agentId = new("ToStringType", "ToStringKey");
+
+ agentId.ToString().Should().Be("ToStringType/ToStringKey");
+ }
+
+ [Fact]
+ public void AgentIdShouldCompareInequalityForWrongTypeTest()
+ {
+ AgentId agentId1 = new("Type1", "Key1");
+
+ (!agentId1.Equals(Guid.NewGuid())).Should().BeTrue();
+ }
+
+ [Fact]
+ public void AgentIdShouldCompareInequalityCorrectlyTest()
+ {
+ AgentId agentId1 = new("Type1", "Key1");
+ AgentId agentId2 = new("Type2", "Key2");
+
+ (agentId1 != agentId2).Should().BeTrue();
+ }
+}
diff --git a/dotnet/src/Agents/Runtime/Abstractions.Tests/AgentMetaDataTests.cs b/dotnet/src/Agents/Runtime/Abstractions.Tests/AgentMetaDataTests.cs
new file mode 100644
index 000000000000..9ad839c52996
--- /dev/null
+++ b/dotnet/src/Agents/Runtime/Abstractions.Tests/AgentMetaDataTests.cs
@@ -0,0 +1,22 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using FluentAssertions;
+using Xunit;
+
+namespace Microsoft.SemanticKernel.Agents.Runtime.Abstractions.Tests;
+
+[Trait("Category", "Unit")]
+public class AgentMetadataTests()
+{
+ [Fact]
+ public void AgentMetadataShouldInitializeCorrectlyTest()
+ {
+ // Arrange & Act
+ AgentMetadata metadata = new("TestType", "TestKey", "TestDescription");
+
+ // Assert
+ metadata.Type.Should().Be("TestType");
+ metadata.Key.Should().Be("TestKey");
+ metadata.Description.Should().Be("TestDescription");
+ }
+}
diff --git a/dotnet/src/Agents/Runtime/Abstractions.Tests/AgentProxyTests.cs b/dotnet/src/Agents/Runtime/Abstractions.Tests/AgentProxyTests.cs
new file mode 100644
index 000000000000..63474724a27e
--- /dev/null
+++ b/dotnet/src/Agents/Runtime/Abstractions.Tests/AgentProxyTests.cs
@@ -0,0 +1,91 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Text.Json;
+using System.Threading;
+using System.Threading.Tasks;
+using Moq;
+using Xunit;
+
+namespace Microsoft.SemanticKernel.Agents.Runtime.Abstractions.Tests;
+
+[Trait("Category", "Unit")]
+public class AgentProxyTests
+{
+ private readonly Mock mockRuntime;
+ private readonly AgentId agentId;
+ private readonly AgentProxy agentProxy;
+
+ public AgentProxyTests()
+ {
+ this.mockRuntime = new Mock();
+ this.agentId = new AgentId("testType", "testKey");
+ this.agentProxy = new AgentProxy(this.agentId, this.mockRuntime.Object);
+ }
+
+ [Fact]
+ public void IdMatchesAgentIdTest()
+ {
+ // Assert
+ Assert.Equal(this.agentId, this.agentProxy.Id);
+ }
+
+ [Fact]
+ public void MetadataShouldMatchAgentTest()
+ {
+ AgentMetadata expectedMetadata = new("testType", "testKey", "testDescription");
+ this.mockRuntime.Setup(r => r.GetAgentMetadataAsync(this.agentId))
+ .ReturnsAsync(expectedMetadata);
+
+ Assert.Equal(expectedMetadata, this.agentProxy.Metadata);
+ }
+
+ [Fact]
+ public async Task SendMessageResponseTest()
+ {
+ // Arrange
+ object message = new { Content = "Hello" };
+ AgentId sender = new("senderType", "senderKey");
+ object response = new { Content = "Response" };
+
+ this.mockRuntime.Setup(r => r.SendMessageAsync(message, this.agentId, sender, null, It.IsAny()))
+ .ReturnsAsync(response);
+
+ // Act
+ object? result = await this.agentProxy.SendMessageAsync(message, sender);
+
+ // Assert
+ Assert.Equal(response, result);
+ }
+
+ [Fact]
+ public async Task LoadStateTest()
+ {
+ // Arrange
+ JsonElement state = JsonDocument.Parse("{\"key\":\"value\"}").RootElement;
+
+ this.mockRuntime.Setup(r => r.LoadAgentStateAsync(this.agentId, state))
+ .Returns(ValueTask.CompletedTask);
+
+ // Act
+ await this.agentProxy.LoadStateAsync(state);
+
+ // Assert
+ this.mockRuntime.Verify(r => r.LoadAgentStateAsync(this.agentId, state), Times.Once);
+ }
+
+ [Fact]
+ public async Task SaveStateTest()
+ {
+ // Arrange
+ JsonElement expectedState = JsonDocument.Parse("{\"key\":\"value\"}").RootElement;
+
+ this.mockRuntime.Setup(r => r.SaveAgentStateAsync(this.agentId))
+ .ReturnsAsync(expectedState);
+
+ // Act
+ JsonElement result = await this.agentProxy.SaveStateAsync();
+
+ // Assert
+ Assert.Equal(expectedState, result);
+ }
+}
diff --git a/dotnet/src/Agents/Runtime/Abstractions.Tests/AgentTypeTests.cs b/dotnet/src/Agents/Runtime/Abstractions.Tests/AgentTypeTests.cs
new file mode 100644
index 000000000000..3bc7ca5d4c2e
--- /dev/null
+++ b/dotnet/src/Agents/Runtime/Abstractions.Tests/AgentTypeTests.cs
@@ -0,0 +1,64 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using Xunit;
+
+namespace Microsoft.SemanticKernel.Agents.Runtime.Abstractions.Tests;
+
+[Trait("Category", "Unit")]
+public class AgentTypeTests
+{
+ [Theory]
+ [InlineData(null)]
+ [InlineData("")]
+ [InlineData(" ")]
+ [InlineData("invalid type")] // Agent type must only contain alphanumeric letters or underscores
+ [InlineData("123invalidType")] // Agent type cannot start with a number
+ [InlineData("invalid@type")] // Agent type must only contain alphanumeric letters or underscores
+ [InlineData("invalid-type")] // Agent type cannot alphanumeric underscores.
+ public void AgentIdShouldThrowArgumentExceptionWithInvalidType(string? invalidType)
+ {
+ // Act & Assert
+ ArgumentException exception = Assert.Throws(() => new AgentType(invalidType!));
+ Assert.Contains("Invalid AgentId type", exception.Message);
+ }
+
+ [Fact]
+ public void ImplicitConversionFromStringTest()
+ {
+ // Arrange
+ string agentTypeName = "TestAgent";
+
+ // Act
+ AgentType agentType = agentTypeName;
+
+ // Assert
+ Assert.Equal(agentTypeName, agentType.Name);
+ }
+
+ [Fact]
+ public void ImplicitConversionToStringTest()
+ {
+ // Arrange
+ AgentType agentType = "TestAgent";
+
+ // Act
+ string agentTypeName = agentType;
+
+ // Assert
+ Assert.Equal("TestAgent", agentTypeName);
+ }
+
+ [Fact]
+ public void ExplicitConversionFromTypeTest()
+ {
+ // Arrange
+ Type type = typeof(string);
+
+ // Act
+ AgentType agentType = (AgentType)type;
+
+ // Assert
+ Assert.Equal(type.Name, agentType.Name);
+ }
+}
diff --git a/dotnet/src/Agents/Runtime/Abstractions.Tests/MessageContextTests.cs b/dotnet/src/Agents/Runtime/Abstractions.Tests/MessageContextTests.cs
new file mode 100644
index 000000000000..f918fcc03a89
--- /dev/null
+++ b/dotnet/src/Agents/Runtime/Abstractions.Tests/MessageContextTests.cs
@@ -0,0 +1,82 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Threading;
+using Xunit;
+
+namespace Microsoft.SemanticKernel.Agents.Runtime.Abstractions.Tests;
+
+[Trait("Category", "Unit")]
+public class MessageContextTests
+{
+ [Fact]
+ public void ConstructWithMessageIdAndCancellationTokenTest()
+ {
+ // Arrange
+ string messageId = Guid.NewGuid().ToString();
+ CancellationToken cancellationToken = new();
+
+ // Act
+ MessageContext messageContext = new(messageId, cancellationToken);
+
+ // Assert
+ Assert.Equal(messageId, messageContext.MessageId);
+ Assert.Equal(cancellationToken, messageContext.CancellationToken);
+ }
+
+ [Fact]
+ public void ConstructWithCancellationTokenTest()
+ {
+ // Arrange
+ CancellationToken cancellationToken = new();
+
+ // Act
+ MessageContext messageContext = new(cancellationToken);
+
+ // Assert
+ Assert.NotNull(messageContext.MessageId);
+ Assert.Equal(cancellationToken, messageContext.CancellationToken);
+ }
+
+ [Fact]
+ public void AssignSenderTest()
+ {
+ // Arrange
+ MessageContext messageContext = new(new CancellationToken());
+ AgentId sender = new("type", "key");
+
+ // Act
+ messageContext.Sender = sender;
+
+ // Assert
+ Assert.Equal(sender, messageContext.Sender);
+ }
+
+ [Fact]
+ public void AssignTopicTest()
+ {
+ // Arrange
+ MessageContext messageContext = new(new CancellationToken());
+ TopicId topic = new("type", "source");
+
+ // Act
+ messageContext.Topic = topic;
+
+ // Assert
+ Assert.Equal(topic, messageContext.Topic);
+ }
+
+ [Fact]
+ public void AssignIsRpcPropertyTest()
+ {
+ // Arrange
+ MessageContext messageContext = new(new CancellationToken())
+ {
+ // Act
+ IsRpc = true
+ };
+
+ // Assert
+ Assert.True(messageContext.IsRpc);
+ }
+}
diff --git a/dotnet/src/Agents/Runtime/Abstractions.Tests/Runtime.Abstractions.Tests.csproj b/dotnet/src/Agents/Runtime/Abstractions.Tests/Runtime.Abstractions.Tests.csproj
new file mode 100644
index 000000000000..e0298ff6577b
--- /dev/null
+++ b/dotnet/src/Agents/Runtime/Abstractions.Tests/Runtime.Abstractions.Tests.csproj
@@ -0,0 +1,30 @@
+
+
+
+ Microsoft.SemanticKernel.Agents.Runtime.Abstractions.UnitTests
+ Microsoft.SemanticKernel.Agents.Runtime.Abstractions.UnitTests
+ net8.0
+ True
+ $(NoWarn);CA1707;CA2007;CA1812;CA1861;CA1063;CS0618;CS1591;IDE1006;VSTHRD111;SKEXP0001;SKEXP0050;SKEXP0110;OPENAI001
+
+
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
+
+
+
diff --git a/dotnet/src/Agents/Runtime/Abstractions.Tests/TopicIdTests.cs b/dotnet/src/Agents/Runtime/Abstractions.Tests/TopicIdTests.cs
new file mode 100644
index 000000000000..69d931ee9200
--- /dev/null
+++ b/dotnet/src/Agents/Runtime/Abstractions.Tests/TopicIdTests.cs
@@ -0,0 +1,184 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using Xunit;
+
+namespace Microsoft.SemanticKernel.Agents.Runtime.Abstractions.Tests;
+
+[Trait("Category", "Unit")]
+public class TopicIdTests
+{
+ [Fact]
+ public void ConstrWithTypeOnlyTest()
+ {
+ // Arrange & Act
+ TopicId topicId = new("testtype");
+
+ // Assert
+ Assert.Equal("testtype", topicId.Type);
+ Assert.Equal(TopicId.DefaultSource, topicId.Source);
+ }
+
+ [Fact]
+ public void ConstructWithTypeAndSourceTest()
+ {
+ // Arrange & Act
+ TopicId topicId = new("testtype", "customsource");
+
+ // Assert
+ Assert.Equal("testtype", topicId.Type);
+ Assert.Equal("customsource", topicId.Source);
+ }
+
+ [Fact]
+ public void ConstructWithTupleTest()
+ {
+ // Arrange
+ (string, string) tuple = ("testtype", "customsource");
+
+ // Act
+ TopicId topicId = new(tuple);
+
+ // Assert
+ Assert.Equal("testtype", topicId.Type);
+ Assert.Equal("customsource", topicId.Source);
+ }
+
+ [Fact]
+ public void ConvertFromStringTest()
+ {
+ // Arrange
+ const string topicIdStr = "testtype/customsource";
+
+ // Act
+ TopicId topicId = TopicId.FromStr(topicIdStr);
+
+ // Assert
+ Assert.Equal("testtype", topicId.Type);
+ Assert.Equal("customsource", topicId.Source);
+ }
+
+ [Theory]
+ [InlineData("invalid-format")]
+ [InlineData("too/many/parts")]
+ [InlineData("")]
+ public void InvalidFormatFromStringThrowsTest(string invalidInput)
+ {
+ // Act & Assert
+ Assert.Throws(() => TopicId.FromStr(invalidInput));
+ }
+
+ [Fact]
+ public void ToStringTest()
+ {
+ // Arrange
+ TopicId topicId = new("testtype", "customsource");
+
+ // Act
+ string result = topicId.ToString();
+
+ // Assert
+ Assert.Equal("testtype/customsource", result);
+ }
+
+ [Fact]
+ public void EqualityTest()
+ {
+ // Arrange
+ TopicId topicId1 = new("testtype", "customsource");
+ TopicId topicId2 = new("testtype", "customsource");
+
+ // Act & Assert
+ Assert.True(topicId1.Equals(topicId2));
+ Assert.True(topicId1.Equals((object)topicId2));
+ }
+
+ [Fact]
+ public void InequalityTest()
+ {
+ // Arrange
+ TopicId topicId1 = new("testtype1", "source1");
+ TopicId topicId2 = new("testtype2", "source2");
+ TopicId topicId3 = new("testtype1", "source2");
+ TopicId topicId4 = new("testtype2", "source1");
+
+ // Act & Assert
+ Assert.False(topicId1.Equals(topicId2));
+ Assert.False(topicId1.Equals(topicId3));
+ Assert.False(topicId1.Equals(topicId4));
+ }
+
+ [Fact]
+ public void NullEqualityTest()
+ {
+ // Arrange
+ TopicId topicId = new("testtype", "customsource");
+
+ // Act & Assert
+ Assert.False(topicId.Equals(null));
+ }
+
+ [Fact]
+ public void DifferentTypeEqualityTest()
+ {
+ // Arrange
+ TopicId topicId = new("testtype", "customsource");
+ const string differentType = "not-a-topic-id";
+
+ // Act & Assert
+ Assert.False(topicId.Equals(differentType));
+ }
+
+ [Fact]
+ public void GetHashCodeTest()
+ {
+ // Arrange
+ TopicId topicId1 = new("testtype", "customsource");
+ TopicId topicId2 = new("testtype", "customsource");
+
+ // Act
+ int hash1 = topicId1.GetHashCode();
+ int hash2 = topicId2.GetHashCode();
+
+ // Assert
+ Assert.Equal(hash1, hash2);
+ }
+
+ [Fact]
+ public void ExplicitConversionTest()
+ {
+ // Arrange
+ string topicIdStr = "testtype/customsource";
+
+ // Act
+ TopicId topicId = (TopicId)topicIdStr;
+
+ // Assert
+ Assert.Equal("testtype", topicId.Type);
+ Assert.Equal("customsource", topicId.Source);
+ }
+
+ [Fact]
+ public void IsWildcardMatchTest()
+ {
+ // Arrange
+ TopicId topicId1 = new("testtype", "source1");
+ TopicId topicId2 = new("testtype", "source2");
+
+ // Act & Assert
+ Assert.True(topicId1.IsWildcardMatch(topicId2));
+ Assert.True(topicId2.IsWildcardMatch(topicId1));
+ }
+
+ [Fact]
+ public void IsWildcardMismatchTest()
+ {
+ // Arrange
+ TopicId topicId1 = new("testtype1", "source");
+ TopicId topicId2 = new("testtype2", "source");
+
+ // Act & Assert
+ Assert.False(topicId1.IsWildcardMatch(topicId2));
+ Assert.False(topicId2.IsWildcardMatch(topicId1));
+ }
+}
diff --git a/dotnet/src/Agents/Runtime/Abstractions/AgentId.cs b/dotnet/src/Agents/Runtime/Abstractions/AgentId.cs
new file mode 100644
index 000000000000..bab4f9b51183
--- /dev/null
+++ b/dotnet/src/Agents/Runtime/Abstractions/AgentId.cs
@@ -0,0 +1,136 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.Text.RegularExpressions;
+using Microsoft.SemanticKernel.Agents.Runtime.Internal;
+
+namespace Microsoft.SemanticKernel.Agents.Runtime;
+
+///
+/// Agent ID uniquely identifies an agent instance within an agent runtime, including a distributed runtime.
+/// It serves as the "address" of the agent instance for receiving messages.
+/// \
+///
+/// See the Python equivalent:
+/// AgentId in AutoGen (Python).
+///
+[DebuggerDisplay($"AgentId(type=\"{{{nameof(Type)}}}\", key=\"{{{nameof(Key)}}}\")")]
+public struct AgentId : IEquatable
+{
+ ///
+ /// The default source value used when no source is explicitly provided.
+ ///
+ public const string DefaultKey = "default";
+
+ private static readonly Regex KeyRegex = new(@"^[\x20-\x7E]+$", RegexOptions.Compiled); // ASCII 32-126
+
+ ///
+ /// An identifier that associates an agent with a specific factory function.
+ /// Strings may only be composed of alphanumeric letters (a-z) and (0-9), or underscores (_).
+ ///
+ public string Type { get; }
+
+ ///
+ /// Agent instance identifier.
+ /// Strings may only be composed of alphanumeric letters (a-z) and (0-9), or underscores (_).
+ ///
+ public string Key { get; }
+
+ internal static Regex KeyRegex1 => KeyRegex2;
+
+ internal static Regex KeyRegex2 => KeyRegex;
+
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// The agent type.
+ /// Agent instance identifier.
+ public AgentId(string type, string key)
+ {
+ AgentType.Validate(type);
+
+ if (string.IsNullOrWhiteSpace(key) || !KeyRegex.IsMatch(key))
+ {
+ throw new ArgumentException($"Invalid AgentId key: '{key}'. Must only contain ASCII characters 32-126.");
+ }
+
+ this.Type = type;
+ this.Key = key;
+ }
+
+ ///
+ /// Initializes a new instance of the struct from a tuple.
+ ///
+ /// A tuple containing the agent type and key.
+ public AgentId((string Type, string Key) kvPair)
+ : this(kvPair.Type, kvPair.Key)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the struct from an .
+ ///
+ /// The agent type.
+ /// Agent instance identifier.
+ public AgentId(AgentType type, string key)
+ : this(type.Name, key)
+ {
+ }
+
+ ///
+ /// Convert a string of the format "type/key" into an .
+ ///
+ /// The agent ID string.
+ /// An instance of .
+ public static AgentId FromStr(string maybeAgentId) => new(maybeAgentId.ToKeyValuePair(nameof(Type), nameof(Key)));
+
+ ///
+ /// Returns the string representation of the .
+ ///
+ /// A string in the format "type/key".
+ public override readonly string ToString() => $"{this.Type}/{this.Key}";
+
+ ///
+ /// Determines whether the specified object is equal to the current .
+ ///
+ /// The object to compare with the current instance.
+ /// true if the specified object is equal to the current ; otherwise, false.
+ public override readonly bool Equals([NotNullWhen(true)] object? obj)
+ {
+ return (obj is AgentId other && this.Equals(other));
+ }
+
+ ///
+ public readonly bool Equals(AgentId other)
+ {
+ return this.Type == other.Type && this.Key == other.Key;
+ }
+
+ ///
+ /// Returns a hash code for this .
+ ///
+ /// A hash code for the current instance.
+ public override readonly int GetHashCode()
+ {
+ return HashCode.Combine(this.Type, this.Key);
+ }
+
+ ///
+ /// Explicitly converts a string to an .
+ ///
+ /// The string representation of an agent ID.
+ /// An instance of .
+ public static explicit operator AgentId(string id) => FromStr(id);
+
+ ///
+ /// Equality operator for .
+ ///
+ public static bool operator ==(AgentId left, AgentId right) => left.Equals(right);
+
+ ///
+ /// Inequality operator for .
+ ///
+ public static bool operator !=(AgentId left, AgentId right) => !left.Equals(right);
+}
diff --git a/dotnet/src/Agents/Runtime/Abstractions/AgentMetadata.cs b/dotnet/src/Agents/Runtime/Abstractions/AgentMetadata.cs
new file mode 100644
index 000000000000..56ca1afe77bf
--- /dev/null
+++ b/dotnet/src/Agents/Runtime/Abstractions/AgentMetadata.cs
@@ -0,0 +1,58 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+
+namespace Microsoft.SemanticKernel.Agents.Runtime;
+
+///
+/// Represents metadata associated with an agent, including its type, unique key, and description.
+///
+public readonly struct AgentMetadata(string type, string key, string description) : IEquatable
+{
+ ///
+ /// An identifier that associates an agent with a specific factory function.
+ /// Strings may only be composed of alphanumeric letters (a-z, 0-9), or underscores (_).
+ ///
+ public string Type { get; } = type;
+
+ ///
+ /// A unique key identifying the agent instance.
+ /// Strings may only be composed of alphanumeric letters (a-z, 0-9), or underscores (_).
+ ///
+ public string Key { get; } = key;
+
+ ///
+ /// A brief description of the agent's purpose or functionality.
+ ///
+ public string Description { get; } = description;
+
+ ///
+ public override readonly bool Equals(object? obj)
+ {
+ return obj is AgentMetadata agentMetadata && this.Equals(agentMetadata);
+ }
+
+ ///
+ public readonly bool Equals(AgentMetadata other)
+ {
+ return this.Type.Equals(other.Type, StringComparison.Ordinal) && this.Key.Equals(other.Key, StringComparison.Ordinal);
+ }
+
+ ///
+ public override readonly int GetHashCode()
+ {
+ return HashCode.Combine(this.Type, this.Key);
+ }
+
+ ///
+ public static bool operator ==(AgentMetadata left, AgentMetadata right)
+ {
+ return left.Equals(right);
+ }
+
+ ///
+ public static bool operator !=(AgentMetadata left, AgentMetadata right)
+ {
+ return !(left == right);
+ }
+}
diff --git a/dotnet/src/Agents/Runtime/Abstractions/AgentProxy.cs b/dotnet/src/Agents/Runtime/Abstractions/AgentProxy.cs
new file mode 100644
index 000000000000..fda0bbd8ee4f
--- /dev/null
+++ b/dotnet/src/Agents/Runtime/Abstractions/AgentProxy.cs
@@ -0,0 +1,85 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Text.Json;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Microsoft.SemanticKernel.Agents.Runtime;
+
+///
+/// A proxy that allows you to use an in place of its associated .
+///
+public class AgentProxy
+{
+ ///
+ /// The runtime instance used to interact with agents.
+ ///
+ private readonly IAgentRuntime _runtime;
+ private AgentMetadata? _metadata;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public AgentProxy(AgentId agentId, IAgentRuntime runtime)
+ {
+ this.Id = agentId;
+ this._runtime = runtime;
+ }
+
+ ///
+ /// The target agent for this proxy.
+ ///
+ public AgentId Id { get; }
+
+ ///
+ /// Gets the metadata of the agent.
+ ///
+ ///
+ /// An instance of containing details about the agent.
+ ///
+ public AgentMetadata Metadata => this._metadata ??= this.QueryMetadataAndUnwrap();
+
+ ///
+ /// Sends a message to the agent and processes the response.
+ ///
+ /// The message to send to the agent.
+ /// The agent that is sending the message.
+ ///
+ /// The message ID. If null, a new message ID will be generated.
+ /// This message ID must be unique and is recommended to be a UUID.
+ ///
+ ///
+ /// A token used to cancel an in-progress operation. Defaults to null.
+ ///
+ /// A task representing the asynchronous operation, returning the response from the agent.
+ public ValueTask