Port Agent Runtime abstractions / inprocess runtime (#149)

This commit is contained in:
Stephen Toub
2025-07-09 09:08:45 -04:00
committed by GitHub
Unverified
parent 31dfdcb3ce
commit 4a0f8dcbe0
97 changed files with 3801 additions and 156 deletions
@@ -0,0 +1,333 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
namespace Microsoft.Extensions.AI.Agents.Runtime.InProcess.Tests;
public class InProcessRuntimeTests()
{
[Fact]
public async Task RuntimeStatusLifecycleTestAsync()
{
// Arrange & Act
await using InProcessRuntime runtime = new();
// Assert
Assert.False(runtime.DeliverToSelf);
Assert.Equal(0, runtime.messageQueueCount);
// Act
await runtime.StopAsync(); // Already stopped
await runtime.RunUntilIdleAsync(); // Never throws
await runtime.StartAsync();
// Assert
// Invalid to start runtime that is already started
await Assert.ThrowsAsync<InvalidOperationException>(() => runtime.StartAsync());
Assert.Equal(0, runtime.messageQueueCount);
// Act
await runtime.StopAsync();
// Assert
Assert.Equal(0, runtime.messageQueueCount);
}
[Fact]
public async Task SubscriptionRegistrationLifecycleTestAsync()
{
// Arrange
await using InProcessRuntime runtime = new();
TestSubscription subscription = new("TestTopic", "MyAgent");
// Act & Assert
await Assert.ThrowsAsync<InvalidOperationException>(async () => await runtime.RemoveSubscriptionAsync(subscription.Id));
// Arrange
await runtime.AddSubscriptionAsync(subscription);
// Act & Assert
await Assert.ThrowsAsync<InvalidOperationException>(async () => await runtime.AddSubscriptionAsync(subscription));
// Act
await runtime.RemoveSubscriptionAsync(subscription.Id);
}
[Fact]
public async Task AgentRegistrationLifecycleTestAsync()
{
// Arrange
const string AgentType = "MyAgent";
const string AgentDescription = "A test agent";
List<MockAgent> agents = [];
await using InProcessRuntime runtime = new();
// Act & Assert
await Assert.ThrowsAsync<InvalidOperationException>(async () => await runtime.GetAgentAsync(AgentType, lazy: false));
// Arrange
await runtime.RegisterAgentFactoryAsync(AgentType, factoryFunc);
// Act & Assert
await Assert.ThrowsAsync<InvalidOperationException>(async () => await runtime.RegisterAgentFactoryAsync(AgentType, factoryFunc));
// Act: Lookup by type
AgentId agentId = await runtime.GetAgentAsync(AgentType, lazy: false);
// Assert
Assert.Single(agents);
Assert.Single(runtime.agentInstances);
// Act
MockAgent agent = await runtime.TryGetUnderlyingAgentInstanceAsync<MockAgent>(agentId);
// Assert
Assert.Equal(agentId, agent.Id);
// Act & Assert
await Assert.ThrowsAsync<InvalidOperationException>(async () => await runtime.TryGetUnderlyingAgentInstanceAsync<WrongAgent>(agentId));
// Act: Lookup by ID
AgentId sameId = await runtime.GetAgentAsync(agentId, lazy: false);
// Assert
Assert.Equal(agentId, sameId);
// Act: Lookup by Type
sameId = await runtime.GetAgentAsync((AgentType)agent.Id.Type, lazy: false);
// Assert
Assert.Equal(agentId, sameId);
// Act: Lookup metadata
AgentMetadata metadata = await runtime.GetAgentMetadataAsync(agentId);
// Assert
Assert.Equal(agentId.Type, metadata.Type);
Assert.Equal(AgentDescription, metadata.Description);
Assert.Equal(agentId.Key, metadata.Key);
// Act: Access proxy
AgentProxy proxy = await runtime.TryGetAgentProxyAsync(agentId);
// Assert
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)
{
MockAgent agent = new(id, runtime, AgentDescription);
agents.Add(agent);
return agent;
}
}
[Fact]
public async Task AgentStateLifecycleTestAsync()
{
// Arrange
const string AgentType = "MyAgent";
const string TestMessage = "test message";
await using InProcessRuntime firstRuntime = new();
await firstRuntime.RegisterAgentFactoryAsync(AgentType, factoryFunc);
// Act
AgentId agentId = await firstRuntime.GetAgentAsync(AgentType, lazy: false);
// Assert
Assert.Single(firstRuntime.agentInstances);
// Arrange
MockAgent agent = (MockAgent)firstRuntime.agentInstances[agentId];
agent.ReceivedMessages.Add(TestMessage);
// Act
JsonElement agentState = await firstRuntime.SaveAgentStateAsync(agentId);
// Arrange
await using InProcessRuntime secondRuntime = new();
await secondRuntime.RegisterAgentFactoryAsync(AgentType, factoryFunc);
// Act
await secondRuntime.LoadAgentStateAsync(agentId, agentState);
// Assert
Assert.Single(secondRuntime.agentInstances);
MockAgent copy = (MockAgent)secondRuntime.agentInstances[agentId];
Assert.Single(copy.ReceivedMessages);
Assert.Equal(TestMessage, copy.ReceivedMessages.Single().ToString());
static async ValueTask<MockAgent> factoryFunc(AgentId id, IAgentRuntime runtime)
{
MockAgent agent = new(id, runtime, "A test agent");
return agent;
}
}
[Fact]
public async Task RuntimeSendMessageTestAsync()
{
// Arrange
await using InProcessRuntime runtime = new();
MockAgent? agent = null;
await runtime.RegisterAgentFactoryAsync("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);
// Assert
Assert.NotNull(agent);
Assert.Empty(agent.ReceivedMessages);
// Act: Send message
await runtime.StartAsync();
await runtime.SendMessageAsync("TestMessage", agent.Id);
await runtime.RunUntilIdleAsync();
// Assert
Assert.Equal(0, runtime.messageQueueCount);
Assert.Single(agent.ReceivedMessages);
}
// Agent will not deliver to self will success when runtime.DeliverToSelf is false (default)
[Theory]
[InlineData(false, 0)]
[InlineData(true, 1)]
public async Task RuntimeAgentPublishToSelfTestAsync(bool selfPublish, int receiveCount)
{
// Arrange
await using InProcessRuntime runtime = new()
{
DeliverToSelf = selfPublish
};
MockAgent? agent = null;
await runtime.RegisterAgentFactoryAsync("MyAgent", async (id, runtime) =>
{
agent = new MockAgent(id, runtime, "A test agent");
return agent;
});
// Assert
Assert.Empty(runtime.agentInstances);
// Act: Ensure the agent is actually created
AgentId agentId = await runtime.GetAgentAsync("MyAgent", lazy: false);
// Assert
Assert.NotNull(agent);
Assert.Single(runtime.agentInstances);
const string TopicType = "TestTopic";
// Arrange
await runtime.AddSubscriptionAsync(new TestSubscription(TopicType, agentId.Type));
// Act
await runtime.StartAsync();
await runtime.PublishMessageAsync("SelfMessage", new TopicId(TopicType), sender: agentId);
await runtime.RunUntilIdleAsync();
// Assert
Assert.Equal(receiveCount, agent.ReceivedMessages.Count);
}
[Fact]
public async Task RuntimeShouldSaveLoadStateCorrectlyTestAsync()
{
// Arrange: Create a runtime and register an agent
await using InProcessRuntime runtime = new();
MockAgent? agent = null;
await runtime.RegisterAgentFactoryAsync("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);
const string TopicType = "TestTopic";
await runtime.AddSubscriptionAsync(new TestSubscription(TopicType, agentId.Type));
await runtime.StartAsync();
await runtime.PublishMessageAsync("test", new TopicId(TopicType));
await runtime.RunUntilIdleAsync();
// Act: Save the state
JsonElement savedState = await runtime.SaveStateAsync();
// Assert: Ensure the agent's state is stored as a valid JSON type
Assert.NotNull(agent);
Assert.True(savedState.TryGetProperty(agentId.ToString(), out JsonElement agentState));
Assert.Equal(JsonValueKind.Array, agentState.ValueKind);
Assert.Single(agent.ReceivedMessages);
// Arrange: Serialize and Deserialize the state to simulate persistence
string json = JsonSerializer.Serialize(savedState);
Assert.NotNull(json);
Assert.NotEmpty(json);
IDictionary<string, JsonElement> deserializedState = JsonSerializer.Deserialize<IDictionary<string, JsonElement>>(json)
?? throw new InvalidOperationException("Deserialized state is unexpectedly null");
Assert.True(deserializedState.ContainsKey(agentId.ToString()));
// Act: Start new runtime and restore the state
agent = null;
await using InProcessRuntime newRuntime = new();
await newRuntime.StartAsync();
await newRuntime.RegisterAgentFactoryAsync("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);
// 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(agent.ReceivedMessages);
}
#pragma warning disable CA1812 // Avoid uninstantiated internal classes
private sealed class WrongAgent : IHostableAgent
#pragma warning restore CA1812
{
public AgentId Id => throw new NotImplementedException();
public AgentMetadata Metadata => throw new NotImplementedException();
public ValueTask CloseAsync() => default;
public ValueTask LoadStateAsync(JsonElement state)
{
throw new NotImplementedException();
}
public ValueTask<object?> OnMessageAsync(object message, MessageContext messageContext)
{
throw new NotImplementedException();
}
public ValueTask<JsonElement> SaveStateAsync()
{
throw new NotImplementedException();
}
}
}
@@ -0,0 +1,101 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Threading;
using System.Threading.Tasks;
namespace Microsoft.Extensions.AI.Agents.Runtime.InProcess.Tests;
public class MessageEnvelopeTests
{
[Fact]
public void ConstructAllParametersTest()
{
// Arrange
object message = new { Content = "Test message" };
const string MessageId = "testid";
CancellationToken cancellation = new();
// Act
MessageEnvelope envelope = new(message, MessageId, cancellation);
// Assert
Assert.Same(message, envelope.Message);
Assert.Equal(MessageId, envelope.MessageId);
Assert.Equal(cancellation, envelope.Cancellation);
Assert.Null(envelope.Sender);
Assert.Null(envelope.Receiver);
Assert.Null(envelope.Topic);
}
[Fact]
public void ConstructOnlyRequiredParametersTest()
{
// Arrange & Act
MessageEnvelope envelope = new("test");
// Assert
Assert.NotNull(envelope.MessageId);
Assert.NotEmpty(envelope.MessageId);
// Verify it's a valid GUID
Assert.True(Guid.TryParse(envelope.MessageId, out _));
}
[Fact]
public void WithSenderTest()
{
// Arrange
MessageEnvelope envelope = new("test");
AgentId sender = new("testtype", "testkey");
// Act
MessageEnvelope result = envelope.WithSender(sender);
// Assert
Assert.Same(envelope, result);
Assert.Equal(sender, envelope.Sender);
}
[Fact]
public async Task ForSendTestAsync()
{
// Arrange
MessageEnvelope envelope = new("test");
AgentId receiver = new("receivertype", "receiverkey");
object expectedResult = new { Response = "Success" };
ValueTask<object?> servicer(MessageEnvelope env, CancellationToken ct) => new(expectedResult);
// Act
MessageDelivery delivery = envelope.ForSend(receiver, servicer);
// Assert
Assert.NotNull(delivery);
Assert.Same(envelope, delivery.Message);
Assert.Equal(receiver, envelope.Receiver);
// 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.Same(expectedResult, result);
}
[Fact]
public void ForPublishTest()
{
// Arrange
MessageEnvelope envelope = new("test");
TopicId topic = new("testtopic");
static ValueTask servicer(MessageEnvelope env, CancellationToken ct) => default;
// Act
MessageDelivery delivery = envelope.ForPublish(topic, servicer);
// Assert
Assert.NotNull(delivery);
Assert.Same(envelope, delivery.Message);
Assert.Equal(topic, envelope.Topic);
}
}
@@ -0,0 +1,202 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace Microsoft.Extensions.AI.Agents.Runtime.InProcess.Tests;
public sealed class BasicMessage
{
public string Content { get; set; } = string.Empty;
}
#pragma warning disable RCS1194 // Implement exception constructors
public sealed class TestException : Exception;
#pragma warning restore RCS1194 // Implement exception constructors
public sealed class PublisherAgent : TestAgent, IHandle<BasicMessage>
{
private readonly IList<TopicId> _targetTopics;
public PublisherAgent(AgentId 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)
{
await this.PublishMessageAsync(
new BasicMessage { Content = $"@{targetTopic}: {item.Content}" },
targetTopic);
}
}
}
public sealed class SendOnAgent : TestAgent, IHandle<BasicMessage>
{
private readonly IList<Guid> _targetKeys;
public SendOnAgent(AgentId 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)
{
AgentId targetId = new(nameof(ReceiverAgent), targetKey.ToString());
BasicMessage response = new() { Content = $"@{targetKey}: {item.Content}" };
await this.SendMessageAsync(response, targetId);
}
}
}
public sealed class ReceiverAgent : TestAgent, IHandle<BasicMessage>
{
public List<BasicMessage> Messages { get; } = [];
public ReceiverAgent(AgentId id, IAgentRuntime runtime, string description)
: base(id, runtime, description)
{
}
public ValueTask HandleAsync(BasicMessage item, MessageContext messageContext)
{
this.Messages.Add(item);
return default;
}
}
public sealed class ProcessorAgent : TestAgent, IHandle<BasicMessage, BasicMessage>
{
private Func<string, string> ProcessFunc { get; }
public ProcessorAgent(AgentId 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);
}
}
public sealed class CancelAgent : TestAgent, IHandle<BasicMessage>
{
public CancelAgent(AgentId 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;
}
}
public sealed class ErrorAgent : TestAgent, IHandle<BasicMessage>
{
public ErrorAgent(AgentId id, IAgentRuntime runtime, string description)
: base(id, runtime, description)
{
}
public bool DidThrow { get; private set; }
public ValueTask HandleAsync(BasicMessage item, MessageContext messageContext)
{
this.DidThrow = true;
throw new TestException();
}
}
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
{
async ValueTask<TAgent> WrappedFactory(AgentId id, IAgentRuntime runtime)
{
TAgent agent = await factory(id, runtime);
this.GetAgentInstances<TAgent>()[id] = agent;
return agent;
}
return this.Runtime.RegisterAgentFactoryAsync(type, WrappedFactory);
}
public Dictionary<AgentId, TAgent> GetAgentInstances<TAgent>() where TAgent : IHostableAgent
{
if (!this.AgentsTypeMap.TryGetValue(typeof(TAgent), out object? maybeAgentMap) ||
maybeAgentMap is not Dictionary<AgentId, TAgent> result)
{
this.AgentsTypeMap[typeof(TAgent)] = result = [];
}
return result;
}
public async ValueTask RegisterReceiverAgentAsync(string? agentNameSuffix = null, params string[] topicTypes)
{
await this.RegisterFactoryMapInstances(
$"{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}"));
}
}
public async ValueTask RegisterErrorAgentAsync(string? agentNameSuffix = null, params string[] topicTypes)
{
await this.RegisterFactoryMapInstances(
$"{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}"));
}
}
public async ValueTask RunPublishTestAsync(TopicId sendTarget, object message, string? messageId = null)
{
messageId ??= Guid.NewGuid().ToString();
await this.Runtime.StartAsync();
await this.Runtime.PublishMessageAsync(message, sendTarget, messageId: messageId);
await this.Runtime.RunUntilIdleAsync();
}
public async ValueTask<object?> RunSendTestAsync(AgentId sendTarget, object message, string? messageId = null)
{
messageId ??= Guid.NewGuid().ToString();
await this.Runtime.StartAsync();
object? result = await this.Runtime.SendMessageAsync(message, sendTarget, messageId: messageId);
await this.Runtime.RunUntilIdleAsync();
return result;
}
}
@@ -0,0 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>$(ProjectsTargetFrameworks)</TargetFrameworks>
<TargetFrameworks Condition="'$(Configuration)' == 'Debug'">$(ProjectsDebugTargetFrameworks)</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Microsoft.Extensions.AI.Agents.Runtime.InProcess\Microsoft.Extensions.AI.Agents.Runtime.InProcess.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,116 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Threading.Tasks;
namespace Microsoft.Extensions.AI.Agents.Runtime.InProcess.Tests;
public class PublishMessageTests
{
[Fact]
public async Task Test_PublishMessage_SuccessAsync()
{
MessagingTestFixture fixture = new();
await fixture.RegisterReceiverAgentAsync(topicTypes: "TestTopic");
await fixture.RegisterReceiverAgentAsync("2", topicTypes: "TestTopic");
await fixture.RunPublishTestAsync(new TopicId("TestTopic"), new BasicMessage { Content = "1" });
var values = fixture.GetAgentInstances<ReceiverAgent>().Values;
Assert.Equal(2, values.Count);
Assert.All(values, receiverAgent =>
{
Assert.NotNull(receiverAgent.Messages);
Assert.Single(receiverAgent.Messages);
Assert.Contains(receiverAgent.Messages, m => m.Content == "1");
});
}
[Fact]
public async Task Test_PublishMessage_SingleFailureAsync()
{
MessagingTestFixture fixture = new();
await fixture.RegisterErrorAgentAsync(topicTypes: "TestTopic");
// Test that we wrap single errors appropriately
var e = await Assert.ThrowsAsync<AggregateException>(async () => await fixture.RunPublishTestAsync(new TopicId("TestTopic"), new BasicMessage { Content = "1" }));
Assert.IsType<TestException>(Assert.Single(e.InnerExceptions));
var values = fixture.GetAgentInstances<ReceiverAgent>().Values;
}
[Fact]
public async Task Test_PublishMessage_MultipleFailuresAsync()
{
MessagingTestFixture fixture = new();
await fixture.RegisterErrorAgentAsync(topicTypes: "TestTopic");
await fixture.RegisterErrorAgentAsync("2", topicTypes: "TestTopic");
// What we are really testing here is that a single exception does not prevent sending to the remaining agents
var e = await Assert.ThrowsAsync<AggregateException>(async () => await fixture.RunPublishTestAsync(new TopicId("TestTopic"), new BasicMessage { Content = "1" }));
Assert.Equal(2, e.InnerExceptions.Count);
Assert.All(e.InnerExceptions, innerException => Assert.IsType<TestException>(innerException));
var values = fixture.GetAgentInstances<ErrorAgent>().Values;
Assert.Equal(2, values.Count);
}
[Fact]
public async Task Test_PublishMessage_MixedSuccessFailureAsync()
{
MessagingTestFixture fixture = new();
await fixture.RegisterReceiverAgentAsync(topicTypes: "TestTopic");
await fixture.RegisterReceiverAgentAsync("2", topicTypes: "TestTopic");
await fixture.RegisterErrorAgentAsync(topicTypes: "TestTopic");
await fixture.RegisterErrorAgentAsync("2", topicTypes: "TestTopic");
// What we are really testing here is that raising exceptions does not prevent sending to the remaining agents
var e = await Assert.ThrowsAsync<AggregateException>(async () => await fixture.RunPublishTestAsync(new TopicId("TestTopic"), new BasicMessage { Content = "1" }));
Assert.Equal(2, e.InnerExceptions.Count);
Assert.All(e.InnerExceptions, innerException => Assert.IsType<TestException>(innerException));
var agents = fixture.GetAgentInstances<ReceiverAgent>().Values;
Assert.Equal(2, agents.Count);
Assert.All(agents, receiverAgent =>
{
Assert.NotNull(receiverAgent.Messages);
Assert.Single(receiverAgent.Messages);
Assert.Contains(receiverAgent.Messages, m => m.Content == "1");
});
var errors = fixture.GetAgentInstances<ErrorAgent>().Values;
Assert.Equal(2, errors.Count);
}
[Fact]
public async Task Test_PublishMessage_RecurrentPublishSucceedsAsync()
{
MessagingTestFixture fixture = new();
await fixture.RegisterFactoryMapInstances(
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.RegisterReceiverAgentAsync(topicTypes: "TestTopic");
await fixture.RegisterReceiverAgentAsync("2", topicTypes: "TestTopic");
await fixture.RunPublishTestAsync(new TopicId("RunTest"), new BasicMessage { Content = "1" });
TopicId testTopicId = new("TestTopic");
var values = fixture.GetAgentInstances<ReceiverAgent>().Values;
Assert.Equal(2, values.Count);
Assert.All(values, receiver =>
{
Assert.NotNull(receiver.Messages);
Assert.Single(receiver.Messages);
Assert.Contains(receiver.Messages, m => m.Content == $"@{testTopicId}: 1");
});
}
}
@@ -0,0 +1,103 @@
// 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");
}
}
@@ -0,0 +1,90 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading.Tasks;
namespace Microsoft.Extensions.AI.Agents.Runtime.InProcess.Tests;
public class SendMessageTests
{
[Fact]
public async Task Test_SendMessage_ReturnsValueAsync()
{
static string ProcessFunc(string s) => $"Processed({s})";
MessagingTestFixture fixture = new();
await fixture.RegisterFactoryMapInstances(nameof(ProcessorAgent),
(id, runtime) => new ValueTask<ProcessorAgent>(new ProcessorAgent(id, runtime, ProcessFunc, string.Empty)));
AgentId 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);
}
[Fact]
public async Task Test_SendMessage_CancellationAsync()
{
MessagingTestFixture fixture = new();
await fixture.RegisterFactoryMapInstances(nameof(CancelAgent),
(id, runtime) => new ValueTask<CancelAgent>(new CancelAgent(id, runtime, string.Empty)));
AgentId targetAgent = new(nameof(CancelAgent), Guid.NewGuid().ToString());
await Assert.ThrowsAsync<OperationCanceledException>(() => fixture.RunSendTestAsync(targetAgent, new BasicMessage { Content = "1" }).AsTask());
}
[Fact]
public async Task Test_SendMessage_ErrorAsync()
{
MessagingTestFixture fixture = new();
await fixture.RegisterFactoryMapInstances(nameof(ErrorAgent),
(id, runtime) => new ValueTask<ErrorAgent>(new ErrorAgent(id, runtime, string.Empty)));
AgentId targetAgent = new(nameof(ErrorAgent), Guid.NewGuid().ToString());
await Assert.ThrowsAsync<TestException>(() => fixture.RunSendTestAsync(targetAgent, new BasicMessage { Content = "1" }).AsTask());
}
[Fact]
public async Task Test_SendMessage_FromSendMessageHandlerAsync()
{
Guid[] targetGuids = [Guid.NewGuid(), Guid.NewGuid()];
MessagingTestFixture fixture = new();
Dictionary<AgentId, SendOnAgent> sendAgents = fixture.GetAgentInstances<SendOnAgent>();
Dictionary<AgentId, ReceiverAgent> receiverAgents = fixture.GetAgentInstances<ReceiverAgent>();
await fixture.RegisterFactoryMapInstances(nameof(SendOnAgent),
(id, runtime) => new ValueTask<SendOnAgent>(new SendOnAgent(id, runtime, string.Empty, targetGuids)));
await fixture.RegisterFactoryMapInstances(nameof(ReceiverAgent),
(id, runtime) => new ValueTask<ReceiverAgent>(new ReceiverAgent(id, runtime, string.Empty)));
AgentId targetAgent = new(nameof(SendOnAgent), Guid.NewGuid().ToString());
BasicMessage input = new() { Content = "Hello" };
Task testTask = fixture.RunSendTestAsync(targetAgent, input).AsTask();
// We do not actually expect to wait the timeout here, but it is still better than waiting the 10 min
// timeout that the tests default to. A failure will fail regardless of what timeout value we set.
TimeSpan timeout = Debugger.IsAttached ? TimeSpan.FromSeconds(120) : TimeSpan.FromSeconds(10);
Task timeoutTask = Task.Delay(timeout);
Task completedTask = await Task.WhenAny([testTask, timeoutTask]);
Assert.Same(testTask, completedTask);
// Check that each of the target agents received the message
foreach (Guid targetKey in targetGuids)
{
AgentId targetId = new(nameof(ReceiverAgent), targetKey.ToString());
Assert.Single(receiverAgents[targetId].Messages);
Assert.Contains(receiverAgents[targetId].Messages, m => m.Content == $"@{targetKey}: {input.Content}");
}
}
}
@@ -0,0 +1,46 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Threading.Tasks;
namespace Microsoft.Extensions.AI.Agents.Runtime.InProcess.Tests;
public abstract class TestAgent : BaseAgent
{
internal List<object> ReceivedMessages = [];
protected TestAgent(AgentId id, IAgentRuntime runtime, string description)
: base(id, runtime, description)
{
}
}
/// <summary>
/// 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 MockAgent(AgentId id, IAgentRuntime runtime, string description)
: base(id, runtime, description) { }
public ValueTask HandleAsync(string item, MessageContext messageContext)
{
this.ReceivedMessages.Add(item);
return default;
}
public override async ValueTask<JsonElement> SaveStateAsync()
{
JsonElement json = JsonSerializer.SerializeToElement(this.ReceivedMessages);
return json;
}
public override ValueTask LoadStateAsync(JsonElement state)
{
this.ReceivedMessages = JsonSerializer.Deserialize<List<object>>(state) ?? throw new InvalidOperationException("Failed to deserialize state");
return default;
}
}
@@ -0,0 +1,34 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Diagnostics.CodeAnalysis;
namespace Microsoft.Extensions.AI.Agents.Runtime.InProcess.Tests;
public class TestSubscription(string topicType, string agentType, string? id = null) : ISubscriptionDefinition
{
public string Id { get; } = id ?? Guid.NewGuid().ToString();
public string TopicType { get; } = topicType;
public AgentId MapToAgent(TopicId topic)
{
if (!this.Matches(topic))
{
throw new InvalidOperationException("TopicId does not match the subscription.");
}
return new AgentId(agentType, topic.Source);
}
public bool Equals(ISubscriptionDefinition? other) => this.Id == other?.Id;
public override bool Equals([NotNullWhen(true)] object? obj) => obj is TestSubscription other && other.Equals(this);
public override int GetHashCode() => this.Id.GetHashCode();
public bool Matches(TopicId topic)
{
return topic.Type == this.TopicType;
}
}