First round of cleanup of runtime abstractions (#156)

This commit is contained in:
Stephen Toub
2025-07-10 07:57:51 -04:00
committed by GitHub
parent a233d31813
commit fbf1f10a8a
76 changed files with 1254 additions and 2079 deletions
@@ -17,54 +17,34 @@ public class AgentIdTests()
public void AgentIdShouldThrowArgumentExceptionWithInvalidKey(string? invalidKey)
{
// Act & Assert
ArgumentException exception = Assert.Throws<ArgumentException>(() => new AgentId("validType", invalidKey!));
Assert.Contains("Invalid AgentId key", exception.Message);
ArgumentException exception = Assert.Throws<ArgumentException>(() => new ActorId("validType", invalidKey!));
Assert.Contains("Invalid ActorId key", exception.Message);
}
[Fact]
public void AgentIdShouldInitializeCorrectlyTest()
{
AgentId agentId = new("TestType", "TestKey");
ActorId agentId = new("TestType", "TestKey");
Assert.Equal("TestType", agentId.Type);
Assert.Equal("TestKey", agentId.Key);
}
[Fact]
public void AgentIdShouldConvertFromTupleTest()
{
(string, string) agentTuple = ("TupleType", "TupleKey");
AgentId agentId = new(agentTuple);
Assert.Equal("TupleType", agentId.Type);
Assert.Equal("TupleKey", agentId.Key);
}
[Fact]
public void AgentIdShouldConvertFromAgentType()
{
AgentType agentType = "TestType";
AgentId agentId = new(agentType, "TestKey");
Assert.Equal("TestType", agentId.Type);
Assert.Equal("TestType", agentId.Type.Name);
Assert.Equal("TestKey", agentId.Key);
}
[Fact]
public void AgentIdShouldParseFromStringTest()
{
AgentId agentId = AgentId.FromStr("ParsedType/ParsedKey");
ActorId agentId = ActorId.Parse("ParsedType/ParsedKey");
Assert.Equal("ParsedType", agentId.Type);
Assert.Equal("ParsedType", agentId.Type.Name);
Assert.Equal("ParsedKey", agentId.Key);
}
[Fact]
public void AgentIdShouldCompareEqualityCorrectlyTest()
{
AgentId agentId1 = new("SameType", "SameKey");
AgentId agentId2 = new("SameType", "SameKey");
AgentId agentId3 = new("DifferentType", "DifferentKey");
ActorId agentId1 = new("SameType", "SameKey");
ActorId agentId2 = new("SameType", "SameKey");
ActorId agentId3 = new("DifferentType", "DifferentKey");
Assert.Equal(agentId2, agentId1);
Assert.NotEqual(agentId3, agentId1);
@@ -75,27 +55,18 @@ public class AgentIdTests()
[Fact]
public void AgentIdShouldGenerateCorrectHashCodeTest()
{
AgentId agentId1 = new("HashType", "HashKey");
AgentId agentId2 = new("HashType", "HashKey");
AgentId agentId3 = new("DifferentType", "DifferentKey");
ActorId agentId1 = new("HashType", "HashKey");
ActorId agentId2 = new("HashType", "HashKey");
ActorId agentId3 = new("DifferentType", "DifferentKey");
Assert.Equal(agentId2.GetHashCode(), agentId1.GetHashCode());
Assert.NotEqual(agentId3.GetHashCode(), agentId1.GetHashCode());
}
[Fact]
public void AgentIdShouldConvertExplicitlyFromStringTest()
{
AgentId agentId = (AgentId)"ConvertedType/ConvertedKey";
Assert.Equal("ConvertedType", agentId.Type);
Assert.Equal("ConvertedKey", agentId.Key);
}
[Fact]
public void AgentIdShouldReturnCorrectToStringTest()
{
AgentId agentId = new("ToStringType", "ToStringKey");
ActorId agentId = new("ToStringType", "ToStringKey");
Assert.Equal("ToStringType/ToStringKey", agentId.ToString());
}
@@ -103,7 +74,7 @@ public class AgentIdTests()
[Fact]
public void AgentIdShouldCompareInequalityForWrongTypeTest()
{
AgentId agentId1 = new("Type1", "Key1");
ActorId agentId1 = new("Type1", "Key1");
Assert.False(agentId1.Equals(Guid.NewGuid()));
}
@@ -111,8 +82,8 @@ public class AgentIdTests()
[Fact]
public void AgentIdShouldCompareInequalityCorrectlyTest()
{
AgentId agentId1 = new("Type1", "Key1");
AgentId agentId2 = new("Type2", "Key2");
ActorId agentId1 = new("Type1", "Key1");
ActorId agentId2 = new("Type2", "Key2");
Assert.True(agentId1 != agentId2);
}
@@ -8,10 +8,10 @@ public class AgentMetadataTests()
public void AgentMetadataShouldInitializeCorrectlyTest()
{
// Arrange & Act
AgentMetadata metadata = new("TestType", "TestKey", "TestDescription");
ActorMetadata metadata = new(new ActorType("TestType"), "TestKey", "TestDescription");
// Assert
Assert.Equal("TestType", metadata.Type);
Assert.Equal("TestType", metadata.Type.Name);
Assert.Equal("TestKey", metadata.Key);
Assert.Equal("TestDescription", metadata.Description);
}
@@ -10,14 +10,14 @@ namespace Microsoft.Extensions.AI.Agents.Runtime.Abstractions.Tests;
public class AgentProxyTests
{
private readonly Mock<IAgentRuntime> _mockRuntime;
private readonly AgentId _agentId;
private readonly AgentProxy _agentProxy;
private readonly ActorId _agentId;
private readonly IdProxyActor _agentProxy;
public AgentProxyTests()
{
this._mockRuntime = new Mock<IAgentRuntime>();
this._agentId = new AgentId("testType", "testKey");
this._agentProxy = new AgentProxy(this._agentId, this._mockRuntime.Object);
this._agentId = new ActorId("testType", "testKey");
this._agentProxy = new IdProxyActor(this._mockRuntime.Object, this._agentId);
}
[Fact]
@@ -30,8 +30,8 @@ public class AgentProxyTests
[Fact]
public void MetadataShouldMatchAgentTest()
{
AgentMetadata expectedMetadata = new("testType", "testKey", "testDescription");
this._mockRuntime.Setup(r => r.GetAgentMetadataAsync(this._agentId))
ActorMetadata expectedMetadata = new(new("testType"), "testKey", "testDescription");
this._mockRuntime.Setup(r => r.GetActorMetadataAsync(this._agentId, default))
.ReturnsAsync(expectedMetadata);
Assert.Equal(expectedMetadata, this._agentProxy.Metadata);
@@ -42,7 +42,7 @@ public class AgentProxyTests
{
// Arrange
object message = new { Content = "Hello" };
AgentId sender = new("senderType", "senderKey");
ActorId sender = new("senderType", "senderKey");
object response = new { Content = "Response" };
this._mockRuntime.Setup(r => r.SendMessageAsync(message, this._agentId, sender, null, It.IsAny<CancellationToken>()))
@@ -61,14 +61,14 @@ public class AgentProxyTests
// Arrange
JsonElement state = JsonDocument.Parse("{\"key\":\"value\"}").RootElement;
this._mockRuntime.Setup(r => r.LoadAgentStateAsync(this._agentId, state))
this._mockRuntime.Setup(r => r.LoadActorStateAsync(this._agentId, state, default))
.Returns(default(ValueTask));
// Act
await this._agentProxy.LoadStateAsync(state);
// Assert
this._mockRuntime.Verify(r => r.LoadAgentStateAsync(this._agentId, state), Times.Once);
this._mockRuntime.Verify(r => r.LoadActorStateAsync(this._agentId, state, default), Times.Once);
}
[Fact]
@@ -77,7 +77,7 @@ public class AgentProxyTests
// Arrange
JsonElement expectedState = JsonDocument.Parse("{\"key\":\"value\"}").RootElement;
this._mockRuntime.Setup(r => r.SaveAgentStateAsync(this._agentId))
this._mockRuntime.Setup(r => r.SaveActorStateAsync(this._agentId, default))
.ReturnsAsync(expectedState);
// Act
@@ -17,46 +17,18 @@ public class AgentTypeTests
public void AgentIdShouldThrowArgumentExceptionWithInvalidType(string? invalidType)
{
// Act & Assert
ArgumentException exception = Assert.Throws<ArgumentException>(() => new AgentType(invalidType!));
Assert.Contains("Invalid AgentId type", exception.Message);
ArgumentException exception = Assert.Throws<ArgumentException>(() => new ActorType(invalidType!));
Assert.Contains("Invalid type", exception.Message);
}
[Fact]
public void ImplicitConversionFromStringTest()
public void ConversionToStringTest()
{
// Arrange
string agentTypeName = "TestAgent";
// Act
AgentType agentType = agentTypeName;
ActorType agentType = new("TestAgent");
// 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);
Assert.Equal("TestAgent", agentType.Name);
Assert.Equal("TestAgent", agentType.ToString());
}
}
@@ -1,80 +1,34 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Threading;
namespace Microsoft.Extensions.AI.Agents.Runtime.Abstractions.Tests;
public class MessageContextTests
{
[Fact]
public void ConstructWithMessageIdAndCancellationTokenTest()
public void Properties_Roundtrip()
{
// Arrange
string messageId = Guid.NewGuid().ToString();
CancellationToken cancellationToken = new();
MessageContext ctx = new();
// Act
MessageContext messageContext = new(messageId, cancellationToken);
string id = ctx.MessageId;
Assert.NotNull(id);
Assert.True(Guid.TryParse(id, out _));
ctx.MessageId = "newid";
Assert.Equal("newid", ctx.MessageId);
// Assert
Assert.Equal(messageId, messageContext.MessageId);
Assert.Equal(cancellationToken, messageContext.CancellationToken);
}
Assert.False(ctx.IsRpc);
ctx.IsRpc = true;
Assert.True(ctx.IsRpc);
[Fact]
public void ConstructWithCancellationTokenTest()
{
// Arrange
CancellationToken cancellationToken = new();
Assert.Null(ctx.Sender);
ActorId sender = new("type", "key");
ctx.Sender = sender;
Assert.Equal(sender, ctx.Sender);
// 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());
Assert.Null(ctx.Topic);
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);
ctx.Topic = topic;
Assert.Equal(topic, ctx.Topic);
}
}
@@ -14,7 +14,6 @@ public class TopicIdTests
// Assert
Assert.Equal("testtype", topicId.Type);
Assert.Equal(TopicId.DefaultSource, topicId.Source);
}
[Fact]
@@ -28,42 +27,28 @@ public class TopicIdTests
Assert.Equal("customsource", topicId.Source);
}
[Fact]
public void ConstructWithTupleTest()
[Theory]
[InlineData("testtype/https://github.com/cloudevents", "testtype", "https://github.com/cloudevents")]
[InlineData("testtype/mailto:cncf-wg-serverless@lists.cncf.io", "testtype", "mailto:cncf-wg-serverless@lists.cncf.io")]
[InlineData("testtype/urn:uuid:6e8bc430-9c3a-11d9-9669-0800200c9a66", "testtype", "urn:uuid:6e8bc430-9c3a-11d9-9669-0800200c9a66")]
[InlineData("testtype//cloudevents/spec/pull/123", "testtype", "/cloudevents/spec/pull/123")]
[InlineData("testtype//sensors/tn-1234567/alerts", "testtype", "/sensors/tn-1234567/alerts")]
[InlineData("testtype/1-555-123-4567", "testtype", "1-555-123-4567")]
public void ParseTest(string input, string expectedType, string expectedSource)
{
// Arrange
(string, string) tuple = ("testtype", "customsource");
TopicId topicId = TopicId.Parse(input);
// 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);
Assert.Equal(expectedType, topicId.Type);
Assert.Equal(expectedSource, topicId.Source);
}
[Theory]
[InlineData("invalid-format")]
[InlineData("too/many/parts")]
[InlineData("")]
public void InvalidFormatFromStringThrowsTest(string invalidInput)
public void InvalidFormatParseThrowsTest(string invalidInput)
{
// Act & Assert
Assert.Throws<FormatException>(() => TopicId.FromStr(invalidInput));
Assert.Throws<FormatException>(() => TopicId.Parse(invalidInput));
}
[Fact]
@@ -141,42 +126,4 @@ public class TopicIdTests
// 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));
}
}
@@ -4,6 +4,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
namespace Microsoft.Extensions.AI.Agents.Runtime.InProcess.Tests;
@@ -18,7 +19,7 @@ public class InProcessRuntimeTests()
// Assert
Assert.False(runtime.DeliverToSelf);
Assert.Equal(0, runtime.messageQueueCount);
Assert.Equal(0, runtime._messageQueueCount);
// Act
await runtime.StopAsync(); // Already stopped
@@ -29,13 +30,13 @@ public class InProcessRuntimeTests()
// Assert
// Invalid to start runtime that is already started
await Assert.ThrowsAsync<InvalidOperationException>(() => runtime.StartAsync());
Assert.Equal(0, runtime.messageQueueCount);
Assert.Equal(0, runtime._messageQueueCount);
// Act
await runtime.StopAsync();
// Assert
Assert.Equal(0, runtime.messageQueueCount);
Assert.Equal(0, runtime._messageQueueCount);
}
[Fact]
@@ -43,7 +44,7 @@ public class InProcessRuntimeTests()
{
// Arrange
await using InProcessRuntime runtime = new();
TestSubscription subscription = new("TestTopic", "MyAgent");
TestSubscription subscription = new("TestTopic", new("MyAgent"));
// Act & Assert
await Assert.ThrowsAsync<InvalidOperationException>(async () => await runtime.RemoveSubscriptionAsync(subscription.Id));
@@ -68,44 +69,44 @@ public class InProcessRuntimeTests()
await using InProcessRuntime runtime = new();
// Act & Assert
await Assert.ThrowsAsync<InvalidOperationException>(async () => await runtime.GetAgentAsync(AgentType, lazy: false));
await Assert.ThrowsAsync<InvalidOperationException>(async () => await runtime.GetActorAsync(AgentType, lazy: false));
// Arrange
await runtime.RegisterAgentFactoryAsync(AgentType, factoryFunc);
await runtime.RegisterActorFactoryAsync(new(AgentType), factoryFunc);
// Act & Assert
await Assert.ThrowsAsync<InvalidOperationException>(async () => await runtime.RegisterAgentFactoryAsync(AgentType, factoryFunc));
await Assert.ThrowsAsync<InvalidOperationException>(async () => await runtime.RegisterActorFactoryAsync(new(AgentType), factoryFunc));
// Act: Lookup by type
AgentId agentId = await runtime.GetAgentAsync(AgentType, lazy: false);
ActorId agentId = await runtime.GetActorAsync(AgentType, lazy: false);
// Assert
Assert.Single(agents);
Assert.Single(runtime.agentInstances);
Assert.Single(runtime._actorInstances);
// Act
MockAgent agent = await runtime.TryGetUnderlyingAgentInstanceAsync<MockAgent>(agentId);
MockAgent agent = await runtime.TryGetUnderlyingActorInstanceAsync<MockAgent>(agentId);
// Assert
Assert.Equal(agentId, agent.Id);
// Act & Assert
await Assert.ThrowsAsync<InvalidOperationException>(async () => await runtime.TryGetUnderlyingAgentInstanceAsync<WrongAgent>(agentId));
await Assert.ThrowsAsync<InvalidOperationException>(async () => await runtime.TryGetUnderlyingActorInstanceAsync<WrongAgent>(agentId));
// Act: Lookup by ID
AgentId sameId = await runtime.GetAgentAsync(agentId, lazy: false);
ActorId sameId = await runtime.GetActorAsync(agentId, lazy: false);
// Assert
Assert.Equal(agentId, sameId);
// Act: Lookup by Type
sameId = await runtime.GetAgentAsync((AgentType)agent.Id.Type, lazy: false);
sameId = await runtime.GetActorAsync((ActorType)agent.Id.Type, lazy: false);
// Assert
Assert.Equal(agentId, sameId);
// Act: Lookup metadata
AgentMetadata metadata = await runtime.GetAgentMetadataAsync(agentId);
ActorMetadata metadata = await runtime.GetActorMetadataAsync(agentId);
// Assert
Assert.Equal(agentId.Type, metadata.Type);
@@ -113,15 +114,16 @@ public class InProcessRuntimeTests()
Assert.Equal(agentId.Key, metadata.Key);
// Act: Access proxy
AgentProxy proxy = await runtime.TryGetAgentProxyAsync(agentId);
IdProxyActor? proxy = await runtime.TryGetActorProxyAsync(agentId);
// Assert
Assert.NotNull(proxy);
Assert.Equal(agentId, proxy.Id);
Assert.Equal(metadata.Type, proxy.Metadata.Type);
Assert.Equal(metadata.Description, proxy.Metadata.Description);
Assert.Equal(metadata.Key, proxy.Metadata.Key);
async ValueTask<MockAgent> factoryFunc(AgentId id, IAgentRuntime runtime)
async ValueTask<MockAgent> factoryFunc(ActorId id, IAgentRuntime runtime)
{
MockAgent agent = new(id, runtime, AgentDescription);
agents.Add(agent);
@@ -137,35 +139,35 @@ public class InProcessRuntimeTests()
const string TestMessage = "test message";
await using InProcessRuntime firstRuntime = new();
await firstRuntime.RegisterAgentFactoryAsync(AgentType, factoryFunc);
await firstRuntime.RegisterActorFactoryAsync(new(AgentType), factoryFunc);
// Act
AgentId agentId = await firstRuntime.GetAgentAsync(AgentType, lazy: false);
ActorId agentId = await firstRuntime.GetActorAsync(AgentType, lazy: false);
// Assert
Assert.Single(firstRuntime.agentInstances);
Assert.Single(firstRuntime._actorInstances);
// Arrange
MockAgent agent = (MockAgent)firstRuntime.agentInstances[agentId];
MockAgent agent = (MockAgent)firstRuntime._actorInstances[agentId];
agent.ReceivedMessages.Add(TestMessage);
// Act
JsonElement agentState = await firstRuntime.SaveAgentStateAsync(agentId);
JsonElement agentState = await firstRuntime.SaveActorStateAsync(agentId);
// Arrange
await using InProcessRuntime secondRuntime = new();
await secondRuntime.RegisterAgentFactoryAsync(AgentType, factoryFunc);
await secondRuntime.RegisterActorFactoryAsync(new(AgentType), factoryFunc);
// Act
await secondRuntime.LoadAgentStateAsync(agentId, agentState);
await secondRuntime.LoadActorStateAsync(agentId, agentState);
// Assert
Assert.Single(secondRuntime.agentInstances);
MockAgent copy = (MockAgent)secondRuntime.agentInstances[agentId];
Assert.Single(secondRuntime._actorInstances);
MockAgent copy = (MockAgent)secondRuntime._actorInstances[agentId];
Assert.Single(copy.ReceivedMessages);
Assert.Equal(TestMessage, copy.ReceivedMessages.Single().ToString());
static async ValueTask<MockAgent> factoryFunc(AgentId id, IAgentRuntime runtime)
static async ValueTask<MockAgent> factoryFunc(ActorId id, IAgentRuntime runtime)
{
MockAgent agent = new(id, runtime, "A test agent");
return agent;
@@ -178,14 +180,14 @@ public class InProcessRuntimeTests()
// Arrange
await using InProcessRuntime runtime = new();
MockAgent? agent = null;
await runtime.RegisterAgentFactoryAsync("MyAgent", async (id, runtime) =>
await runtime.RegisterActorFactoryAsync(new("MyAgent"), async (id, runtime) =>
{
agent = new MockAgent(id, runtime, "A test agent");
return agent;
});
// Act: Ensure the agent is actually created
AgentId agentId = await runtime.GetAgentAsync("MyAgent", lazy: false);
ActorId agentId = await runtime.GetActorAsync("MyAgent", lazy: false);
// Assert
Assert.NotNull(agent);
@@ -197,7 +199,7 @@ public class InProcessRuntimeTests()
await runtime.RunUntilIdleAsync();
// Assert
Assert.Equal(0, runtime.messageQueueCount);
Assert.Equal(0, runtime._messageQueueCount);
Assert.Single(agent.ReceivedMessages);
}
@@ -214,21 +216,21 @@ public class InProcessRuntimeTests()
};
MockAgent? agent = null;
await runtime.RegisterAgentFactoryAsync("MyAgent", async (id, runtime) =>
await runtime.RegisterActorFactoryAsync(new("MyAgent"), async (id, runtime) =>
{
agent = new MockAgent(id, runtime, "A test agent");
return agent;
});
// Assert
Assert.Empty(runtime.agentInstances);
Assert.Empty(runtime._actorInstances);
// Act: Ensure the agent is actually created
AgentId agentId = await runtime.GetAgentAsync("MyAgent", lazy: false);
ActorId agentId = await runtime.GetActorAsync("MyAgent", lazy: false);
// Assert
Assert.NotNull(agent);
Assert.Single(runtime.agentInstances);
Assert.Single(runtime._actorInstances);
const string TopicType = "TestTopic";
@@ -250,14 +252,14 @@ public class InProcessRuntimeTests()
// Arrange: Create a runtime and register an agent
await using InProcessRuntime runtime = new();
MockAgent? agent = null;
await runtime.RegisterAgentFactoryAsync("MyAgent", async (id, runtime) =>
await runtime.RegisterActorFactoryAsync(new("MyAgent"), async (id, runtime) =>
{
agent = new MockAgent(id, runtime, "test agent");
return agent;
});
// Get agent ID and instantiate agent by publishing
AgentId agentId = await runtime.GetAgentAsync("MyAgent", lazy: false);
ActorId agentId = await runtime.GetActorAsync("MyAgent", lazy: false);
const string TopicType = "TestTopic";
await runtime.AddSubscriptionAsync(new TestSubscription(TopicType, agentId.Type));
@@ -286,46 +288,44 @@ public class InProcessRuntimeTests()
agent = null;
await using InProcessRuntime newRuntime = new();
await newRuntime.StartAsync();
await newRuntime.RegisterAgentFactoryAsync("MyAgent", async (id, runtime) =>
await newRuntime.RegisterActorFactoryAsync(new("MyAgent"), async (id, runtime) =>
{
agent = new MockAgent(id, runtime, "another agent");
return agent;
});
// Assert: Show that no agent instances exist in the new runtime
Assert.Empty(newRuntime.agentInstances);
Assert.Empty(newRuntime._actorInstances);
// Act: Load the state into the new runtime and show that agent is now instantiated
await newRuntime.LoadStateAsync(savedState);
// Assert
Assert.NotNull(agent);
Assert.Single(newRuntime.agentInstances);
Assert.True(newRuntime.agentInstances.ContainsKey(agentId));
Assert.Single(newRuntime._actorInstances);
Assert.True(newRuntime._actorInstances.ContainsKey(agentId));
Assert.Single(agent.ReceivedMessages);
}
#pragma warning disable CA1812 // Avoid uninstantiated internal classes
private sealed class WrongAgent : IHostableAgent
private sealed class WrongAgent : IRuntimeActor
#pragma warning restore CA1812
{
public AgentId Id => throw new NotImplementedException();
public ActorId Id => throw new NotImplementedException();
public AgentMetadata Metadata => throw new NotImplementedException();
public ActorMetadata Metadata => throw new NotImplementedException();
public ValueTask CloseAsync() => default;
public ValueTask LoadStateAsync(JsonElement state)
public ValueTask LoadStateAsync(JsonElement state, CancellationToken cancellationToken = default)
{
throw new NotImplementedException();
}
public ValueTask<object?> OnMessageAsync(object message, MessageContext messageContext)
public ValueTask<object?> OnMessageAsync(object message, MessageContext messageContext, CancellationToken cancellationToken = default)
{
throw new NotImplementedException();
}
public ValueTask<JsonElement> SaveStateAsync()
public ValueTask<JsonElement> SaveStateAsync(CancellationToken cancellationToken = default)
{
throw new NotImplementedException();
}
@@ -46,7 +46,7 @@ public class MessageEnvelopeTests
{
// Arrange
MessageEnvelope envelope = new("test");
AgentId sender = new("testtype", "testkey");
ActorId sender = new("testtype", "testkey");
// Act
MessageEnvelope result = envelope.WithSender(sender);
@@ -61,7 +61,7 @@ public class MessageEnvelopeTests
{
// Arrange
MessageEnvelope envelope = new("test");
AgentId receiver = new("receivertype", "receiverkey");
ActorId receiver = new("receivertype", "receiverkey");
object expectedResult = new { Response = "Success" };
ValueTask<object?> servicer(MessageEnvelope env, CancellationToken ct) => new(expectedResult);
@@ -76,8 +76,8 @@ public class MessageEnvelopeTests
// Invoke the servicer to verify result sink works
await delivery.InvokeAsync(CancellationToken.None);
Assert.True(delivery.ResultSink.Future.IsCompleted);
object? result = await delivery.ResultSink.Future;
Assert.True(delivery.ResultTask.IsCompleted);
object? result = await delivery.ResultTask;
Assert.Same(expectedResult, result);
}
@@ -16,114 +16,87 @@ public sealed class BasicMessage
public sealed class TestException : Exception;
#pragma warning restore RCS1194 // Implement exception constructors
public sealed class PublisherAgent : TestAgent, IHandle<BasicMessage>
public sealed class PublisherAgent : TestAgent
{
private readonly IList<TopicId> _targetTopics;
public PublisherAgent(AgentId id, IAgentRuntime runtime, string description, IList<TopicId> targetTopics)
: base(id, runtime, description)
public PublisherAgent(ActorId id, IAgentRuntime runtime, string description, IList<TopicId> targetTopics) : base(id, runtime, description)
{
this._targetTopics = targetTopics;
}
public async ValueTask HandleAsync(BasicMessage item, MessageContext messageContext)
{
this.ReceivedMessages.Add(item);
foreach (TopicId targetTopic in this._targetTopics)
this.RegisterMessageHandler<BasicMessage>(async (item, messageContext, cancellationToken) =>
{
await this.PublishMessageAsync(
new BasicMessage { Content = $"@{targetTopic}: {item.Content}" },
targetTopic);
}
this.ReceivedMessages.Add(item);
foreach (TopicId targetTopic in targetTopics)
{
await this.PublishMessageAsync(
new BasicMessage { Content = $"@{targetTopic}: {item.Content}" },
targetTopic,
cancellationToken: cancellationToken);
}
});
}
}
public sealed class SendOnAgent : TestAgent, IHandle<BasicMessage>
public sealed class SendOnAgent : TestAgent
{
private readonly IList<Guid> _targetKeys;
public SendOnAgent(AgentId id, IAgentRuntime runtime, string description, IList<Guid> targetKeys)
: base(id, runtime, description)
public SendOnAgent(ActorId id, IAgentRuntime runtime, string description, IList<Guid> targetKeys) : base(id, runtime, description)
{
this._targetKeys = targetKeys;
}
public async ValueTask HandleAsync(BasicMessage item, MessageContext messageContext)
{
foreach (Guid targetKey in this._targetKeys)
this.RegisterMessageHandler<BasicMessage>(async (item, messageContext, cancellationToken) =>
{
AgentId targetId = new(nameof(ReceiverAgent), targetKey.ToString());
BasicMessage response = new() { Content = $"@{targetKey}: {item.Content}" };
await this.SendMessageAsync(response, targetId);
}
foreach (Guid targetKey in targetKeys)
{
ActorId targetId = new(nameof(ReceiverAgent), targetKey.ToString());
BasicMessage response = new() { Content = $"@{targetKey}: {item.Content}" };
await this.SendMessageAsync(response, targetId, cancellationToken: cancellationToken);
}
});
}
}
public sealed class ReceiverAgent : TestAgent, IHandle<BasicMessage>
public sealed class ReceiverAgent : TestAgent
{
public List<BasicMessage> Messages { get; } = [];
public ReceiverAgent(AgentId id, IAgentRuntime runtime, string description)
: base(id, runtime, description)
public ReceiverAgent(ActorId id, IAgentRuntime runtime, string description) : base(id, runtime, description)
{
}
public ValueTask HandleAsync(BasicMessage item, MessageContext messageContext)
{
this.Messages.Add(item);
return default;
this.RegisterMessageHandler<BasicMessage>(async (item, messageContext, cancellationToken) =>
{
this.Messages.Add(item);
});
}
}
public sealed class ProcessorAgent : TestAgent, IHandle<BasicMessage, BasicMessage>
public sealed class ProcessorAgent : TestAgent
{
private Func<string, string> ProcessFunc { get; }
public ProcessorAgent(AgentId id, IAgentRuntime runtime, Func<string, string> processFunc, string description)
: base(id, runtime, description)
public ProcessorAgent(ActorId id, IAgentRuntime runtime, Func<string, string> processFunc, string description) : base(id, runtime, description)
{
this.ProcessFunc = processFunc;
}
public ValueTask<BasicMessage> HandleAsync(BasicMessage item, MessageContext messageContext)
{
BasicMessage result = new() { Content = this.ProcessFunc.Invoke(((BasicMessage)item).Content) };
return new(result);
this.RegisterMessageHandler<BasicMessage, BasicMessage>(async (item, messageContext, cancellationtoken) =>
{
return new BasicMessage() { Content = processFunc.Invoke(((BasicMessage)item).Content) };
});
}
}
public sealed class CancelAgent : TestAgent, IHandle<BasicMessage>
public sealed class CancelAgent : TestAgent
{
public CancelAgent(AgentId id, IAgentRuntime runtime, string description)
: base(id, runtime, description)
public CancelAgent(ActorId id, IAgentRuntime runtime, string description) : base(id, runtime, description)
{
}
public ValueTask HandleAsync(BasicMessage item, MessageContext messageContext)
{
CancellationToken cancelledToken = new(canceled: true);
cancelledToken.ThrowIfCancellationRequested();
return default;
this.RegisterMessageHandler<BasicMessage>(async (item, messageContext, cancellationToken) =>
{
CancellationToken cancelledToken = new(canceled: true);
cancelledToken.ThrowIfCancellationRequested();
});
}
}
public sealed class ErrorAgent : TestAgent, IHandle<BasicMessage>
public sealed class ErrorAgent : TestAgent
{
public ErrorAgent(AgentId id, IAgentRuntime runtime, string description)
: base(id, runtime, description)
public ErrorAgent(ActorId id, IAgentRuntime runtime, string description) : base(id, runtime, description)
{
this.RegisterMessageHandler<BasicMessage>(async (item, messageContext, cancellationToken) =>
{
this.DidThrow = true;
throw new TestException();
});
}
public bool DidThrow { get; private set; }
public ValueTask HandleAsync(BasicMessage item, MessageContext messageContext)
{
this.DidThrow = true;
throw new TestException();
}
}
public sealed class MessagingTestFixture
@@ -131,23 +104,23 @@ public sealed class MessagingTestFixture
private Dictionary<Type, object> AgentsTypeMap { get; } = [];
public InProcessRuntime Runtime { get; } = new();
public ValueTask<AgentType> RegisterFactoryMapInstances<TAgent>(AgentType type, Func<AgentId, IAgentRuntime, ValueTask<TAgent>> factory)
where TAgent : IHostableAgent
public ValueTask<ActorType> RegisterFactoryMapInstances<TAgent>(ActorType type, Func<ActorId, IAgentRuntime, ValueTask<TAgent>> factory)
where TAgent : IRuntimeActor
{
async ValueTask<TAgent> WrappedFactory(AgentId id, IAgentRuntime runtime)
async ValueTask<TAgent> WrappedFactory(ActorId id, IAgentRuntime runtime)
{
TAgent agent = await factory(id, runtime);
this.GetAgentInstances<TAgent>()[id] = agent;
return agent;
}
return this.Runtime.RegisterAgentFactoryAsync(type, WrappedFactory);
return this.Runtime.RegisterActorFactoryAsync(type, WrappedFactory);
}
public Dictionary<AgentId, TAgent> GetAgentInstances<TAgent>() where TAgent : IHostableAgent
public Dictionary<ActorId, TAgent> GetAgentInstances<TAgent>() where TAgent : IRuntimeActor
{
if (!this.AgentsTypeMap.TryGetValue(typeof(TAgent), out object? maybeAgentMap) ||
maybeAgentMap is not Dictionary<AgentId, TAgent> result)
maybeAgentMap is not Dictionary<ActorId, TAgent> result)
{
this.AgentsTypeMap[typeof(TAgent)] = result = [];
}
@@ -157,24 +130,24 @@ public sealed class MessagingTestFixture
public async ValueTask RegisterReceiverAgentAsync(string? agentNameSuffix = null, params string[] topicTypes)
{
await this.RegisterFactoryMapInstances(
$"{nameof(ReceiverAgent)}{agentNameSuffix ?? string.Empty}",
new($"{nameof(ReceiverAgent)}{agentNameSuffix ?? string.Empty}"),
(id, runtime) => new ValueTask<ReceiverAgent>(new ReceiverAgent(id, runtime, string.Empty)));
foreach (string topicType in topicTypes)
{
await this.Runtime.AddSubscriptionAsync(new TestSubscription(topicType, $"{nameof(ReceiverAgent)}{agentNameSuffix ?? string.Empty}"));
await this.Runtime.AddSubscriptionAsync(new TestSubscription(topicType, new($"{nameof(ReceiverAgent)}{agentNameSuffix ?? string.Empty}")));
}
}
public async ValueTask RegisterErrorAgentAsync(string? agentNameSuffix = null, params string[] topicTypes)
{
await this.RegisterFactoryMapInstances(
$"{nameof(ErrorAgent)}{agentNameSuffix ?? string.Empty}",
new($"{nameof(ErrorAgent)}{agentNameSuffix ?? string.Empty}"),
(id, runtime) => new ValueTask<ErrorAgent>(new ErrorAgent(id, runtime, string.Empty)));
foreach (string topicType in topicTypes)
{
await this.Runtime.AddSubscriptionAsync(new TestSubscription(topicType, $"{nameof(ErrorAgent)}{agentNameSuffix ?? string.Empty}"));
await this.Runtime.AddSubscriptionAsync(new TestSubscription(topicType, new($"{nameof(ErrorAgent)}{agentNameSuffix ?? string.Empty}")));
}
}
@@ -187,7 +160,7 @@ public sealed class MessagingTestFixture
await this.Runtime.RunUntilIdleAsync();
}
public async ValueTask<object?> RunSendTestAsync(AgentId sendTarget, object message, string? messageId = null)
public async ValueTask<object?> RunSendTestAsync(ActorId sendTarget, object message, string? messageId = null)
{
messageId ??= Guid.NewGuid().ToString();
@@ -93,10 +93,10 @@ public class PublishMessageTests
MessagingTestFixture fixture = new();
await fixture.RegisterFactoryMapInstances(
nameof(PublisherAgent),
new(nameof(PublisherAgent)),
(id, runtime) => new ValueTask<PublisherAgent>(new PublisherAgent(id, runtime, string.Empty, [new TopicId("TestTopic")])));
await fixture.Runtime.AddSubscriptionAsync(new TestSubscription("RunTest", nameof(PublisherAgent)));
await fixture.Runtime.AddSubscriptionAsync(new TestSubscription("RunTest", new(nameof(PublisherAgent))));
await fixture.RegisterReceiverAgentAsync(topicTypes: "TestTopic");
await fixture.RegisterReceiverAgentAsync("2", topicTypes: "TestTopic");
@@ -1,103 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Threading.Tasks;
using System.Threading.Tasks.Sources;
namespace Microsoft.Extensions.AI.Agents.Runtime.InProcess.Tests;
public class ResultSinkTests
{
[Fact]
public void GetResultTest()
{
// Arrange
ResultSink<int> sink = new();
const int ExpectedResult = 42;
// Act
sink.SetResult(ExpectedResult);
int result = sink.GetResult(0);
// Assert
Assert.Equal(ExpectedResult, result);
Assert.Equal(ValueTaskSourceStatus.Succeeded, sink.GetStatus(0));
}
[Fact]
public async Task FutureResultTestAsync()
{
// Arrange
ResultSink<string> sink = new();
const string ExpectedResult = "test";
// Act
sink.SetResult(ExpectedResult);
string result = await sink.Future;
// Assert
Assert.Equal(ExpectedResult, result);
Assert.Equal(ValueTaskSourceStatus.Succeeded, sink.GetStatus(0));
}
[Fact]
public async Task SetExceptionTestAsync()
{
// Arrange
ResultSink<int> sink = new();
InvalidOperationException expectedException = new("Test exception");
// Act
sink.SetException(expectedException);
// Assert
Exception exception = await Assert.ThrowsAsync<InvalidOperationException>(async () => await sink.Future);
Assert.Equal(expectedException.Message, exception.Message);
exception = Assert.Throws<InvalidOperationException>(() => sink.GetResult(0));
Assert.Equal(expectedException.Message, exception.Message);
Assert.Equal(ValueTaskSourceStatus.Faulted, sink.GetStatus(0));
}
[Fact]
public async Task SetCancelledTestAsync()
{
// Arrange
ResultSink<int> sink = new();
// Act
sink.SetCancelled();
// Assert
Assert.True(sink.IsCancelled);
Assert.Throws<OperationCanceledException>(() => sink.GetResult(0));
await Assert.ThrowsAsync<OperationCanceledException>(async () => await sink.Future);
Assert.Equal(ValueTaskSourceStatus.Canceled, sink.GetStatus(0));
}
[Fact]
public void OnCompletedTest()
{
// Arrange
ResultSink<int> sink = new();
bool continuationCalled = false;
const int ExpectedResult = 42;
// Register the continuation
sink.OnCompleted(
state => continuationCalled = true,
state: null,
token: 0,
ValueTaskSourceOnCompletedFlags.None);
// Assert
Assert.False(continuationCalled, "Continuation should have been called");
// Act
sink.SetResult(ExpectedResult);
// Assert
Assert.Equal(ExpectedResult, sink.GetResult(0));
Assert.Equal(ValueTaskSourceStatus.Succeeded, sink.GetStatus(0));
Assert.True(continuationCalled, "Continuation should have been called");
}
}
@@ -16,10 +16,10 @@ public class SendMessageTests
MessagingTestFixture fixture = new();
await fixture.RegisterFactoryMapInstances(nameof(ProcessorAgent),
await fixture.RegisterFactoryMapInstances(new(nameof(ProcessorAgent)),
(id, runtime) => new ValueTask<ProcessorAgent>(new ProcessorAgent(id, runtime, ProcessFunc, string.Empty)));
AgentId targetAgent = new(nameof(ProcessorAgent), Guid.NewGuid().ToString());
ActorId targetAgent = new(nameof(ProcessorAgent), Guid.NewGuid().ToString());
object? maybeResult = await fixture.RunSendTestAsync(targetAgent, new BasicMessage { Content = "1" });
Assert.Equal("Processed(1)", Assert.IsType<BasicMessage>(maybeResult).Content);
@@ -30,12 +30,12 @@ public class SendMessageTests
{
MessagingTestFixture fixture = new();
await fixture.RegisterFactoryMapInstances(nameof(CancelAgent),
await fixture.RegisterFactoryMapInstances(new(nameof(CancelAgent)),
(id, runtime) => new ValueTask<CancelAgent>(new CancelAgent(id, runtime, string.Empty)));
AgentId targetAgent = new(nameof(CancelAgent), Guid.NewGuid().ToString());
ActorId targetAgent = new(nameof(CancelAgent), Guid.NewGuid().ToString());
await Assert.ThrowsAsync<OperationCanceledException>(() => fixture.RunSendTestAsync(targetAgent, new BasicMessage { Content = "1" }).AsTask());
await Assert.ThrowsAnyAsync<OperationCanceledException>(() => fixture.RunSendTestAsync(targetAgent, new BasicMessage { Content = "1" }).AsTask());
}
[Fact]
@@ -43,10 +43,10 @@ public class SendMessageTests
{
MessagingTestFixture fixture = new();
await fixture.RegisterFactoryMapInstances(nameof(ErrorAgent),
await fixture.RegisterFactoryMapInstances(new(nameof(ErrorAgent)),
(id, runtime) => new ValueTask<ErrorAgent>(new ErrorAgent(id, runtime, string.Empty)));
AgentId targetAgent = new(nameof(ErrorAgent), Guid.NewGuid().ToString());
ActorId targetAgent = new(nameof(ErrorAgent), Guid.NewGuid().ToString());
await Assert.ThrowsAsync<TestException>(() => fixture.RunSendTestAsync(targetAgent, new BasicMessage { Content = "1" }).AsTask());
}
@@ -58,16 +58,16 @@ public class SendMessageTests
MessagingTestFixture fixture = new();
Dictionary<AgentId, SendOnAgent> sendAgents = fixture.GetAgentInstances<SendOnAgent>();
Dictionary<AgentId, ReceiverAgent> receiverAgents = fixture.GetAgentInstances<ReceiverAgent>();
Dictionary<ActorId, SendOnAgent> sendAgents = fixture.GetAgentInstances<SendOnAgent>();
Dictionary<ActorId, ReceiverAgent> receiverAgents = fixture.GetAgentInstances<ReceiverAgent>();
await fixture.RegisterFactoryMapInstances(nameof(SendOnAgent),
await fixture.RegisterFactoryMapInstances(new(nameof(SendOnAgent)),
(id, runtime) => new ValueTask<SendOnAgent>(new SendOnAgent(id, runtime, string.Empty, targetGuids)));
await fixture.RegisterFactoryMapInstances(nameof(ReceiverAgent),
await fixture.RegisterFactoryMapInstances(new(nameof(ReceiverAgent)),
(id, runtime) => new ValueTask<ReceiverAgent>(new ReceiverAgent(id, runtime, string.Empty)));
AgentId targetAgent = new(nameof(SendOnAgent), Guid.NewGuid().ToString());
ActorId targetAgent = new(nameof(SendOnAgent), Guid.NewGuid().ToString());
BasicMessage input = new() { Content = "Hello" };
Task testTask = fixture.RunSendTestAsync(targetAgent, input).AsTask();
@@ -82,7 +82,7 @@ public class SendMessageTests
// Check that each of the target agents received the message
foreach (Guid targetKey in targetGuids)
{
AgentId targetId = new(nameof(ReceiverAgent), targetKey.ToString());
ActorId targetId = new(nameof(ReceiverAgent), targetKey.ToString());
Assert.Single(receiverAgents[targetId].Messages);
Assert.Contains(receiverAgents[targetId].Messages, m => m.Content == $"@{targetKey}: {input.Content}");
}
@@ -3,15 +3,16 @@
using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
namespace Microsoft.Extensions.AI.Agents.Runtime.InProcess.Tests;
public abstract class TestAgent : BaseAgent
public abstract class TestAgent : RuntimeActor
{
internal List<object> ReceivedMessages = [];
protected TestAgent(AgentId id, IAgentRuntime runtime, string description)
protected TestAgent(ActorId id, IAgentRuntime runtime, string description)
: base(id, runtime, description)
{
}
@@ -21,24 +22,26 @@ public abstract class TestAgent : BaseAgent
/// A test agent that captures the messages it receives and
/// is able to save and load its state.
/// </summary>
public sealed class MockAgent : TestAgent, IHandle<string>
public sealed class MockAgent : TestAgent
{
public MockAgent(AgentId id, IAgentRuntime runtime, string description)
: base(id, runtime, description) { }
public MockAgent(ActorId id, IAgentRuntime runtime, string description) : base(id, runtime, description)
{
this.RegisterMessageHandler<string>(this.HandleAsync);
}
public ValueTask HandleAsync(string item, MessageContext messageContext)
public ValueTask HandleAsync(string item, MessageContext messageContext, CancellationToken cancellationToken)
{
this.ReceivedMessages.Add(item);
return default;
}
public override async ValueTask<JsonElement> SaveStateAsync()
public override async ValueTask<JsonElement> SaveStateAsync(CancellationToken cancellationToken = default)
{
JsonElement json = JsonSerializer.SerializeToElement(this.ReceivedMessages);
return json;
}
public override ValueTask LoadStateAsync(JsonElement state)
public override ValueTask LoadStateAsync(JsonElement state, CancellationToken cancellationToken = default)
{
this.ReceivedMessages = JsonSerializer.Deserialize<List<object>>(state) ?? throw new InvalidOperationException("Failed to deserialize state");
return default;
@@ -5,20 +5,20 @@ using System.Diagnostics.CodeAnalysis;
namespace Microsoft.Extensions.AI.Agents.Runtime.InProcess.Tests;
public class TestSubscription(string topicType, string agentType, string? id = null) : ISubscriptionDefinition
public class TestSubscription(string topicType, ActorType agentType, string? id = null) : ISubscriptionDefinition
{
public string Id { get; } = id ?? Guid.NewGuid().ToString();
public string TopicType { get; } = topicType;
public AgentId MapToAgent(TopicId topic)
public ActorId MapToActor(TopicId topic)
{
if (!this.Matches(topic))
{
throw new InvalidOperationException("TopicId does not match the subscription.");
}
return new AgentId(agentType, topic.Source);
return new ActorId(agentType, topic.Source);
}
public bool Equals(ISubscriptionDefinition? other) => this.Id == other?.Id;