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
@@ -0,0 +1,116 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Diagnostics.CodeAnalysis;
namespace Microsoft.Extensions.AI.Agents.Runtime;
/// <summary>
/// Provides a unique identifier for an actor instance within an agent runtime,
/// serving as the "address" of the actor instance for receiving messages.
/// </summary>
public readonly struct ActorId : IEquatable<ActorId>
{
/// <summary>
/// Initializes a new instance of the <see cref="ActorId"/> struct from an <see cref="ActorType"/>.
/// </summary>
/// <param name="type">The actor type.</param>
/// <param name="key">Actor instance identifier.</param>
public ActorId(string type, string key) : this(new ActorType(type), key)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ActorId"/> struct from an <see cref="ActorType"/>.
/// </summary>
/// <param name="type">The actor type.</param>
/// <param name="key">Actor instance identifier.</param>
public ActorId(ActorType type, string key)
{
if (!IsValidKey(key))
{
throw new ArgumentException($"Invalid {nameof(ActorId)} key.", nameof(key));
}
this.Type = type;
this.Key = key;
}
/// <summary>
/// Gets an identifier that associates an actor with a specific factory function.
/// </summary>
/// <remarks>
/// Strings may only be composed of alphanumeric letters (a-z) and (0-9), or underscores (_).
/// </remarks>
public ActorType Type { get; }
/// <summary>
/// Gets an actor instance identifier.
/// </summary>
/// <remarks>
/// Strings may only be composed of alphanumeric letters (a-z) and (0-9), or underscores (_).
/// </remarks>
public string Key { get; }
/// <summary>
/// Convert a string of the format "type/key" into an <see cref="ActorId"/>.
/// </summary>
/// <param name="actorId">The actor ID string.</param>
/// <returns>An instance of <see cref="ActorId"/>.</returns>
public static ActorId Parse(string actorId)
{
if (!KeyValueParser.TryParse(actorId, out string? type, out string? key))
{
throw new FormatException($"Invalid actor ID: '{actorId}'. Expected format is 'type/key'.");
}
return new ActorId(type, key);
}
/// <inheritdoc />
public override readonly string ToString() => $"{this.Type}/{this.Key}";
/// <inheritdoc />
public override readonly bool Equals([NotNullWhen(true)] object? obj) =>
obj is ActorId other && this.Equals(other);
/// <inheritdoc/>
public readonly bool Equals(ActorId other) =>
this.Type == other.Type && this.Key == other.Key;
/// <inheritdoc />
public override readonly int GetHashCode() =>
HashCode.Combine(this.Type, this.Key);
/// <inheritdoc />
public static bool operator ==(ActorId left, ActorId right) =>
left.Equals(right);
/// <inheritdoc />
public static bool operator !=(ActorId left, ActorId right) =>
!left.Equals(right);
/// <summary>Determines whether the specified key is valid.</summary>
/// <remarks>It must be non-null, not be only whitespace, and only contain printable ASCII characters.</remarks>
internal static bool IsValidKey(string key)
{
if (string.IsNullOrWhiteSpace(key))
{
return false;
}
#if NET
return !key.AsSpan().ContainsAnyExceptInRange((char)32, (char)126);
#else
foreach (char c in key)
{
if ((int)c is < 32 or > 126)
{
return false;
}
}
return true;
#endif
}
}
@@ -0,0 +1,72 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
namespace Microsoft.Extensions.AI.Agents.Runtime;
/// <summary>
/// Represents metadata associated with an actor, including its type, unique key, and description.
/// </summary>
public readonly struct ActorMetadata : IEquatable<ActorMetadata>
{
/// <summary>
/// Initializes a new instance of the <see cref="ActorMetadata"/> class with the specified type, key, and description.
/// </summary>
/// <param name="type">The type of the actor.</param>
/// <param name="key">The unique key associated with the actor.</param>
/// <param name="description">A brief description of the actor.</param>
public ActorMetadata(ActorType type, string key, string description)
{
if (!ActorId.IsValidKey(key))
{
throw new ArgumentException("Invalid actor key.", nameof(key));
}
if (description is null)
{
throw new ArgumentNullException(nameof(description));
}
this.Type = type;
this.Key = key;
this.Description = description;
}
/// <summary>
/// Gets an identifier that associates an actor with a specific factory function.
/// </summary>
public ActorType Type { get; }
/// <summary>
/// A unique key identifying the actor instance.
/// Strings may only be composed of alphanumeric letters (a-z, 0-9), or underscores (_).
/// </summary>
public string Key { get; }
/// <summary>
/// A brief description of the actor's purpose or functionality.
/// </summary>
public string Description { get; }
/// <inheritdoc/>
public override readonly bool Equals(object? obj) =>
obj is ActorMetadata actorMetadata && this.Equals(actorMetadata);
/// <inheritdoc/>
public readonly bool Equals(ActorMetadata other) =>
this.Type == other.Type &&
this.Key == other.Key &&
this.Description == other.Description;
/// <inheritdoc/>
public override readonly int GetHashCode() =>
HashCode.Combine(this.Type, this.Key, this.Description);
/// <inheritdoc/>
public static bool operator ==(ActorMetadata left, ActorMetadata right) =>
left.Equals(right);
/// <inheritdoc/>
public static bool operator !=(ActorMetadata left, ActorMetadata right) =>
!(left == right);
}
@@ -0,0 +1,68 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Text.RegularExpressions;
namespace Microsoft.Extensions.AI.Agents.Runtime;
/// <summary>
/// Represents the type of an actor.
/// </summary>
public readonly partial struct ActorType : IEquatable<ActorType>
{
/// <summary>
/// Initializes a new instance of the <see cref="ActorId"/> struct.
/// </summary>
/// <param name="type">The actor type.</param>
public ActorType(string type)
{
if (!IsValid(type))
{
throw new ArgumentException($"Invalid type: '{type}'. Must be alphanumeric (a-z, 0-9, _) and cannot start with a number or contain spaces.");
}
this.Name = type;
}
/// <summary>
/// The string representation of this actor type.
/// </summary>
public string Name { get; }
/// <summary>
/// Returns the string representation of the <see cref="ActorType"/>.
/// </summary>
/// <returns>A string in the format "type/source".</returns>
public override readonly string ToString() =>
this.Name;
/// <inheritdoc/>
public override bool Equals(object? obj) =>
obj is ActorType other && this.Equals(other);
/// <inheritdoc/>
public bool Equals(ActorType other) =>
this.Name.Equals(other.Name, StringComparison.Ordinal);
/// <inheritdoc/>
public override int GetHashCode() =>
this.Name.GetHashCode();
/// <inheritdoc/>
public static bool operator ==(ActorType left, ActorType right) =>
left.Equals(right);
/// <inheritdoc/>
public static bool operator !=(ActorType left, ActorType right) =>
!(left == right);
internal static bool IsValid(string type) =>
type is not null && TypeRegex().IsMatch(type);
#if NET
[GeneratedRegex("^[a-zA-Z_][a-zA-Z_0-9]*$")]
private static partial Regex TypeRegex();
#else
private static Regex TypeRegex() => new("^[a-zA-Z_][a-zA-Z_0-9]*$", RegexOptions.Compiled);
#endif
}
@@ -1,135 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Text.RegularExpressions;
namespace Microsoft.Extensions.AI.Agents.Runtime;
/// <summary>
/// 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.
/// </summary>\
/// <remarks>
/// See the Python equivalent:
/// <see href="https://github.com/microsoft/agent-runtime/blob/main/python/agent_runtime/core/agent_id.py">AgentId in AutoGen (Python)</see>.
/// </remarks>
[DebuggerDisplay($"AgentId(type=\"{{{nameof(Type)}}}\", key=\"{{{nameof(Key)}}}\")")]
public struct AgentId : IEquatable<AgentId>
{
/// <summary>
/// The default source value used when no source is explicitly provided.
/// </summary>
public const string DefaultKey = "default";
private static readonly Regex KeyRegex = new(@"^[\x20-\x7E]+$", RegexOptions.Compiled); // ASCII 32-126
/// <summary>
/// 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 (_).
/// </summary>
public string Type { get; }
/// <summary>
/// Agent instance identifier.
/// Strings may only be composed of alphanumeric letters (a-z) and (0-9), or underscores (_).
/// </summary>
public string Key { get; }
internal static Regex KeyRegex1 => KeyRegex2;
internal static Regex KeyRegex2 => KeyRegex;
/// <summary>
/// Initializes a new instance of the <see cref="AgentId"/> struct.
/// </summary>
/// <param name="type">The agent type.</param>
/// <param name="key">Agent instance identifier.</param>
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;
}
/// <summary>
/// Initializes a new instance of the <see cref="AgentId"/> struct from a tuple.
/// </summary>
/// <param name="kvPair">A tuple containing the agent type and key.</param>
public AgentId((string Type, string Key) kvPair)
: this(kvPair.Type, kvPair.Key)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="AgentId"/> struct from an <see cref="AgentType"/>.
/// </summary>
/// <param name="type">The agent type.</param>
/// <param name="key">Agent instance identifier.</param>
public AgentId(AgentType type, string key)
: this(type.Name, key)
{
}
/// <summary>
/// Convert a string of the format "type/key" into an <see cref="AgentId"/>.
/// </summary>
/// <param name="maybeAgentId">The agent ID string.</param>
/// <returns>An instance of <see cref="AgentId"/>.</returns>
public static AgentId FromStr(string maybeAgentId) => new(maybeAgentId.ToKeyValuePair(nameof(Type), nameof(Key)));
/// <summary>
/// Returns the string representation of the <see cref="AgentId"/>.
/// </summary>
/// <returns>A string in the format "type/key".</returns>
public override readonly string ToString() => $"{this.Type}/{this.Key}";
/// <summary>
/// Determines whether the specified object is equal to the current <see cref="AgentId"/>.
/// </summary>
/// <param name="obj">The object to compare with the current instance.</param>
/// <returns><c>true</c> if the specified object is equal to the current <see cref="AgentId"/>; otherwise, <c>false</c>.</returns>
public override readonly bool Equals([NotNullWhen(true)] object? obj)
{
return (obj is AgentId other && this.Equals(other));
}
/// <inheritdoc/>
public readonly bool Equals(AgentId other)
{
return this.Type == other.Type && this.Key == other.Key;
}
/// <summary>
/// Returns a hash code for this <see cref="AgentId"/>.
/// </summary>
/// <returns>A hash code for the current instance.</returns>
public override readonly int GetHashCode()
{
return HashCode.Combine(this.Type, this.Key);
}
/// <summary>
/// Explicitly converts a string to an <see cref="AgentId"/>.
/// </summary>
/// <param name="id">The string representation of an agent ID.</param>
/// <returns>An instance of <see cref="AgentId"/>.</returns>
public static explicit operator AgentId(string id) => FromStr(id);
/// <summary>
/// Equality operator for <see cref="AgentId"/>.
/// </summary>
public static bool operator ==(AgentId left, AgentId right) => left.Equals(right);
/// <summary>
/// Inequality operator for <see cref="AgentId"/>.
/// </summary>
public static bool operator !=(AgentId left, AgentId right) => !left.Equals(right);
}
@@ -1,58 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
namespace Microsoft.Extensions.AI.Agents.Runtime;
/// <summary>
/// Represents metadata associated with an agent, including its type, unique key, and description.
/// </summary>
public readonly struct AgentMetadata(string type, string key, string description) : IEquatable<AgentMetadata>
{
/// <summary>
/// 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 (_).
/// </summary>
public string Type { get; } = type;
/// <summary>
/// A unique key identifying the agent instance.
/// Strings may only be composed of alphanumeric letters (a-z, 0-9), or underscores (_).
/// </summary>
public string Key { get; } = key;
/// <summary>
/// A brief description of the agent's purpose or functionality.
/// </summary>
public string Description { get; } = description;
/// <inheritdoc/>
public override readonly bool Equals(object? obj)
{
return obj is AgentMetadata agentMetadata && this.Equals(agentMetadata);
}
/// <inheritdoc/>
public readonly bool Equals(AgentMetadata other)
{
return this.Type.Equals(other.Type, StringComparison.Ordinal) && this.Key.Equals(other.Key, StringComparison.Ordinal);
}
/// <inheritdoc/>
public override readonly int GetHashCode()
{
return HashCode.Combine(this.Type, this.Key);
}
/// <inheritdoc/>
public static bool operator ==(AgentMetadata left, AgentMetadata right)
{
return left.Equals(right);
}
/// <inheritdoc/>
public static bool operator !=(AgentMetadata left, AgentMetadata right)
{
return !(left == right);
}
}
@@ -1,85 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
namespace Microsoft.Extensions.AI.Agents.Runtime;
/// <summary>
/// A proxy that allows you to use an <see cref="AgentId"/> in place of its associated <see cref="IAgent"/>.
/// </summary>
public class AgentProxy
{
/// <summary>
/// The runtime instance used to interact with agents.
/// </summary>
private readonly IAgentRuntime _runtime;
private AgentMetadata? _metadata;
/// <summary>
/// Initializes a new instance of the <see cref="AgentProxy"/> class.
/// </summary>
public AgentProxy(AgentId agentId, IAgentRuntime runtime)
{
this.Id = agentId;
this._runtime = runtime;
}
/// <summary>
/// The target agent for this proxy.
/// </summary>
public AgentId Id { get; }
/// <summary>
/// Gets the metadata of the agent.
/// </summary>
/// <value>
/// An instance of <see cref="AgentMetadata"/> containing details about the agent.
/// </value>
public AgentMetadata Metadata => this._metadata ??= this.QueryMetadataAndUnwrap();
/// <summary>
/// Sends a message to the agent and processes the response.
/// </summary>
/// <param name="message">The message to send to the agent.</param>
/// <param name="sender">The agent that is sending the message.</param>
/// <param name="messageId">
/// The message ID. If <c>null</c>, a new message ID will be generated.
/// This message ID must be unique and is recommended to be a UUID.
/// </param>
/// <param name="cancellationToken">
/// A token used to cancel an in-progress operation. Defaults to <c>null</c>.
/// </param>
/// <returns>A task representing the asynchronous operation, returning the response from the agent.</returns>
public ValueTask<object?> SendMessageAsync(object message, AgentId sender, string? messageId = null, CancellationToken cancellationToken = default)
{
return this._runtime.SendMessageAsync(message, this.Id, sender, messageId, cancellationToken);
}
/// <summary>
/// Loads the state of the agent from a previously saved state.
/// </summary>
/// <param name="state">A dictionary representing the state of the agent. Must be JSON serializable.</param>
/// <returns>A task representing the asynchronous operation.</returns>
public ValueTask LoadStateAsync(JsonElement state)
{
return this._runtime.LoadAgentStateAsync(this.Id, state);
}
/// <summary>
/// Saves the state of the agent. The result must be JSON serializable.
/// </summary>
/// <returns>A task representing the asynchronous operation, returning a dictionary containing the saved state.</returns>
public ValueTask<JsonElement> SaveStateAsync()
{
return this._runtime.SaveAgentStateAsync(this.Id);
}
private AgentMetadata QueryMetadataAndUnwrap()
{
#pragma warning disable VSTHRD002 // Avoid problematic synchronous waits
return this._runtime.GetAgentMetadataAsync(this.Id).AsTask().ConfigureAwait(false).GetAwaiter().GetResult();
#pragma warning restore VSTHRD002 // Avoid problematic synchronous waits
}
}
@@ -1,103 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Text.RegularExpressions;
namespace Microsoft.Extensions.AI.Agents.Runtime;
/// <summary>
/// Represents the type of an agent as a string.
/// This is a strongly-typed wrapper around a string, ensuring type safety when working with agent types.
/// </summary>
/// <remarks>
/// This struct is immutable and provides implicit conversion to and from <see cref="string"/>.
/// </remarks>
public readonly partial struct AgentType : IEquatable<AgentType>
{
#if NET
[GeneratedRegex("^[a-zA-Z_][a-zA-Z0-9_]*$")]
private static partial Regex TypeRegex();
#else
private static Regex TypeRegex() => new("^[a-zA-Z_][a-zA-Z0-9_]*$", RegexOptions.Compiled);
#endif
internal static void Validate(string type)
{
if (string.IsNullOrWhiteSpace(type) || !TypeRegex().IsMatch(type))
{
throw new ArgumentException($"Invalid AgentId type: '{type}'. Must be alphanumeric (a-z, 0-9, _) and cannot start with a number or contain spaces.");
}
}
/// <summary>
/// Initializes a new instance of the <see cref="AgentId"/> struct.
/// </summary>
/// <param name="type">The agent type.</param>
public AgentType(string type)
{
Validate(type);
this.Name = type;
}
/// <summary>
/// The string representation of this agent type.
/// </summary>
public string Name { get; }
/// <summary>
/// Returns the string representation of the <see cref="AgentType"/>.
/// </summary>
/// <returns>A string in the format "type/source".</returns>
public override readonly string ToString() => this.Name;
/// <summary>
/// Explicitly converts a <see cref="Type"/> to an <see cref="AgentType"/>.
/// </summary>
/// <param name="type">The .NET <see cref="Type"/> to convert.</param>
/// <returns>An <see cref="AgentType"/> instance with the name of the provided type.</returns>
public static explicit operator AgentType(Type type) => new(type.Name);
/// <summary>
/// Implicitly converts a <see cref="string"/> to an <see cref="AgentType"/>.
/// </summary>
/// <param name="type">The string representation of the agent type.</param>
/// <returns>An <see cref="AgentType"/> instance with the given name.</returns>
public static implicit operator AgentType(string type) => new(type);
/// <summary>
/// Implicitly converts an <see cref="AgentType"/> to a <see cref="string"/>.
/// </summary>
/// <param name="type">The <see cref="AgentType"/> instance.</param>
/// <returns>The string representation of the agent type.</returns>
public static implicit operator string(AgentType type) => type.ToString();
/// <inheritdoc/>
public override bool Equals(object? obj)
{
return obj is AgentType other && this.Equals(other);
}
/// <inheritdoc/>
public bool Equals(AgentType other)
{
return this.Name.Equals(other.Name, StringComparison.Ordinal);
}
/// <inheritdoc/>
public override int GetHashCode()
{
return this.Name.GetHashCode();
}
/// <inheritdoc/>
public static bool operator ==(AgentType left, AgentType right)
{
return left.Equals(right);
}
/// <inheritdoc/>
public static bool operator !=(AgentType left, AgentType right)
{
return !(left == right);
}
}
@@ -1,149 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
namespace Microsoft.Extensions.AI.Agents.Runtime;
/// <summary>
/// Represents the base class for an agent in the AutoGen system.
/// </summary>
public abstract class BaseAgent : IHostableAgent
{
private static readonly JsonElement s_emptyElement = JsonDocument.Parse("{}").RootElement;
/// <summary>
/// The activity source for tracing.
/// </summary>
public static readonly ActivitySource TraceSource = new($"{typeof(IAgent).Namespace}");
private readonly Dictionary<Type, HandlerInvoker> _handlerInvokers;
private readonly IAgentRuntime _runtime;
/// <summary>
/// Provides logging capabilities used for diagnostic and operational information.
/// </summary>
protected internal ILogger Logger { get; }
/// <summary>
/// Gets the description of the agent.
/// </summary>
protected string Description { get; }
/// <summary>
/// Gets the unique identifier of the agent.
/// </summary>
public AgentId Id { get; }
/// <summary>
/// Gets the metadata of the agent.
/// </summary>
public AgentMetadata Metadata { get; }
/// <summary>
/// Initializes a new instance of the BaseAgent class with the specified identifier, runtime, description, and optional logger.
/// </summary>
/// <param name="id">The unique identifier of the agent.</param>
/// <param name="runtime">The runtime environment in which the agent operates.</param>
/// <param name="description">A brief description of the agent's purpose.</param>
/// <param name="logger">An optional logger for recording diagnostic information.</param>
protected BaseAgent(
AgentId id,
IAgentRuntime runtime,
string description,
ILogger? logger = null)
{
this.Logger = logger ?? NullLogger.Instance;
this.Id = id;
this.Description = description;
this.Metadata = new AgentMetadata(this.Id.Type, this.Id.Key, this.Description);
this._runtime = runtime;
this._handlerInvokers = HandlerInvoker.ReflectAgentHandlers(this);
}
/// <summary>
/// Handles an incoming message by determining its type and invoking the corresponding handler method if available.
/// </summary>
/// <param name="message">The message object to be handled.</param>
/// <param name="messageContext">The context associated with the message.</param>
/// <returns>A ValueTask that represents the asynchronous operation, containing the response object or null.</returns>
public async ValueTask<object?> OnMessageAsync(object message, MessageContext messageContext)
{
// Determine type of message, then get handler method and invoke it
Type messageType = message.GetType();
if (this._handlerInvokers.TryGetValue(messageType, out HandlerInvoker? handlerInvoker))
{
return await handlerInvoker.InvokeAsync(message, messageContext).ConfigureAwait(false);
}
return null;
}
/// <inheritdoc/>
public virtual ValueTask<JsonElement> SaveStateAsync()
{
return new ValueTask<JsonElement>(s_emptyElement);
}
/// <inheritdoc/>
public virtual ValueTask LoadStateAsync(JsonElement state) =>
default;
/// <summary>
/// Closes this agent gracefully by releasing allocated resources and performing any necessary cleanup.
/// </summary>
public virtual ValueTask CloseAsync() =>
default;
/// <summary>
/// Sends a message to a specified recipient agent through the runtime.
/// </summary>
/// <param name="agent">The requested agent's type.</param>
/// <param name="cancellationToken">A token used to cancel the operation if needed.</param>
/// <returns>A ValueTask that represents the asynchronous operation, returning the response object or null.</returns>
protected async ValueTask<AgentId?> GetAgentAsync(AgentType agent, CancellationToken cancellationToken = default)
{
try
{
return await this._runtime.GetAgentAsync(agent, lazy: false).ConfigureAwait(false);
}
catch (InvalidOperationException)
{
return null;
}
}
/// <summary>
/// Sends a message to a specified recipient agent through the runtime.
/// </summary>
/// <param name="message">The message object to send.</param>
/// <param name="recipient">The recipient agent's identifier.</param>
/// <param name="messageId">An optional identifier for the message.</param>
/// <param name="cancellationToken">A token used to cancel the operation if needed.</param>
/// <returns>A ValueTask that represents the asynchronous operation, returning the response object or null.</returns>
protected ValueTask<object?> SendMessageAsync(object message, AgentId recipient, string? messageId = null, CancellationToken cancellationToken = default)
{
return this._runtime.SendMessageAsync(message, recipient, sender: this.Id, messageId, cancellationToken);
}
/// <summary>
/// Publishes a message to all agents subscribed to a specific topic through the runtime.
/// </summary>
/// <param name="message">The message object to publish.</param>
/// <param name="topic">The topic identifier to which the message is published.</param>
/// <param name="messageId">An optional identifier for the message.</param>
/// <param name="cancellationToken">A token used to cancel the operation if needed.</param>
/// <returns>A ValueTask that represents the asynchronous publish operation.</returns>
protected ValueTask PublishMessageAsync(object message, TopicId topic, string? messageId = null, CancellationToken cancellationToken = default)
{
return this._runtime.PublishMessageAsync(message, topic, sender: this.Id, messageId, cancellationToken);
}
}
@@ -1,31 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Diagnostics.CodeAnalysis;
namespace Microsoft.Extensions.AI.Agents.Runtime;
/// <summary>
/// Exception thrown when a handler cannot process the given message.
/// </summary>
[ExcludeFromCodeCoverage]
public class CantHandleException : Exception
{
/// <summary>
/// Initializes a new instance of the <see cref="CantHandleException"/> class.
/// </summary>
public CantHandleException() : base("The handler cannot process the given message.") { }
/// <summary>
/// Initializes a new instance of the <see cref="CantHandleException"/> class with a custom error message.
/// </summary>
/// <param name="message">The custom error message.</param>
public CantHandleException(string message) : base(message) { }
/// <summary>
/// Initializes a new instance of the <see cref="CantHandleException"/> class with a custom error message and an inner exception.
/// </summary>
/// <param name="message">The custom error message.</param>
/// <param name="innerException">The inner exception that caused this error.</param>
public CantHandleException(string message, Exception innerException) : base(message, innerException) { }
}
@@ -1,31 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Diagnostics.CodeAnalysis;
namespace Microsoft.Extensions.AI.Agents.Runtime;
/// <summary>
/// Exception thrown when a message is dropped.
/// </summary>
[ExcludeFromCodeCoverage]
public class MessageDroppedException : Exception
{
/// <summary>
/// Initializes a new instance of the <see cref="MessageDroppedException"/> class.
/// </summary>
public MessageDroppedException() : base("The message was dropped.") { }
/// <summary>
/// Initializes a new instance of the <see cref="MessageDroppedException"/> class with a custom error message.
/// </summary>
/// <param name="message">The custom error message.</param>
public MessageDroppedException(string message) : base(message) { }
/// <summary>
/// Initializes a new instance of the <see cref="MessageDroppedException"/> class with a custom error message and an inner exception.
/// </summary>
/// <param name="message">The custom error message.</param>
/// <param name="innerException">The inner exception that caused this error.</param>
public MessageDroppedException(string message, Exception innerException) : base(message, innerException) { }
}
@@ -1,31 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Diagnostics.CodeAnalysis;
namespace Microsoft.Extensions.AI.Agents.Runtime;
/// <summary>
/// Exception thrown when an attempt is made to access an unavailable value, such as a remote resource.
/// </summary>
[ExcludeFromCodeCoverage]
public class NotAccessibleException : Exception
{
/// <summary>
/// Initializes a new instance of the <see cref="NotAccessibleException"/> class.
/// </summary>
public NotAccessibleException() : base("The requested value is not accessible.") { }
/// <summary>
/// Initializes a new instance of the <see cref="NotAccessibleException"/> class with a custom error message.
/// </summary>
/// <param name="message">The custom error message.</param>
public NotAccessibleException(string message) : base(message) { }
/// <summary>
/// Initializes a new instance of the <see cref="NotAccessibleException"/> class with a custom error message and an inner exception.
/// </summary>
/// <param name="message">The custom error message.</param>
/// <param name="innerException">The inner exception that caused this error.</param>
public NotAccessibleException(string message, Exception innerException) : base(message, innerException) { }
}
@@ -1,30 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Diagnostics.CodeAnalysis;
namespace Microsoft.Extensions.AI.Agents.Runtime;
/// <summary>
/// Exception thrown when a message cannot be delivered.
/// </summary>
[ExcludeFromCodeCoverage]
public class UndeliverableException : Exception
{
/// <summary>
/// Initializes a new instance of the <see cref="UndeliverableException"/> class.
/// </summary>
public UndeliverableException() : base("The message cannot be delivered.") { }
/// <summary>
/// Initializes a new instance of the <see cref="UndeliverableException"/> class with a custom error message.
/// </summary>
/// <param name="message">The custom error message.</param>
public UndeliverableException(string message) : base(message) { }
/// <summary>
/// Initializes a new instance of the <see cref="UndeliverableException"/> class with a custom error message and an inner exception.
/// </summary>
/// <param name="message">The custom error message.</param>
/// <param name="innerException">The inner exception that caused this error.</param>
public UndeliverableException(string message, Exception innerException) : base(message, innerException) { }
}
@@ -1,138 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
namespace Microsoft.Extensions.AI.Agents.Runtime;
/// <summary>
/// Invokes handler methods asynchronously using reflection.
/// The target methods must return either a ValueTask or a ValueTask{T}.
/// This class wraps the reflection call and provides a unified asynchronous invocation interface.
/// </summary>
internal sealed class HandlerInvoker
{
/// <summary>
/// Scans the provided agent for implemented handler interfaces (IHandle&lt;&gt; and IHandle&lt;,&gt;) via reflection,
/// creates a corresponding <see cref="HandlerInvoker"/> for each handler method, and returns a dictionary that maps
/// the message type (first generic argument of the interface) to its invoker.
/// </summary>
/// <param name="agent">The agent instance whose handler interfaces will be reflected.</param>
/// <returns>A dictionary mapping message types to their corresponding <see cref="HandlerInvoker"/> instances.</returns>
public static Dictionary<Type, HandlerInvoker> ReflectAgentHandlers(BaseAgent agent)
{
Type realType = agent.GetType();
IEnumerable<Type> candidateInterfaces =
realType.GetInterfaces()
.Where(i => i.IsGenericType &&
(i.GetGenericTypeDefinition() == typeof(IHandle<>) ||
(i.GetGenericTypeDefinition() == typeof(IHandle<,>))));
Dictionary<Type, HandlerInvoker> invokers = [];
foreach (Type interface_ in candidateInterfaces)
{
MethodInfo handleAsync =
interface_.GetMethod(nameof(IHandle<object>.HandleAsync), BindingFlags.Instance | BindingFlags.Public) ??
throw new InvalidOperationException($"No handler method found for interface {interface_.FullName}");
HandlerInvoker invoker = new(handleAsync, agent);
invokers.Add(interface_.GetGenericArguments()[0], invoker);
}
return invokers;
}
/// <summary>
/// Represents the asynchronous invocation function.
/// </summary>
private Func<object?, MessageContext, ValueTask<object?>> Invocation { get; }
/// <summary>
/// Initializes a new instance of the <see cref="HandlerInvoker"/> class with the specified method information and target object.
/// </summary>
/// <param name="methodInfo">The MethodInfo representing the handler method to be invoked.</param>
/// <param name="target">The target instance of the agent.</param>
/// <exception cref="InvalidOperationException">Thrown if the target is missing for a non-static method or if the method's return type is not supported.</exception>
private HandlerInvoker(MethodInfo methodInfo, BaseAgent target)
{
object? invocation(object? message, MessageContext messageContext) => methodInfo.Invoke(target, [message, messageContext]);
Func<object?, MessageContext, ValueTask<object?>> getResultAsync;
// Check if the method returns a non-generic ValueTask
if (methodInfo.ReturnType.IsAssignableFrom(typeof(ValueTask)))
{
getResultAsync = async (message, messageContext) =>
{
// Await the ValueTask and return null as there is no result value.
await ((ValueTask)invocation(message, messageContext)!).ConfigureAwait(false);
return null;
};
}
// Check if the method returns a generic ValueTask<T>
else if (methodInfo.ReturnType.IsGenericType && methodInfo.ReturnType.GetGenericTypeDefinition() == typeof(ValueTask<>))
{
// Obtain the generic type argument for ValueTask<T>
MethodInfo typeEraseAwait = typeof(HandlerInvoker)
.GetMethod(nameof(TypeEraseAwaitAsync), BindingFlags.NonPublic | BindingFlags.Static)!
.MakeGenericMethod(methodInfo.ReturnType.GetGenericArguments()[0]);
getResultAsync = async (message, messageContext) =>
{
// Execute the invocation and then type-erase the ValueTask<T> to ValueTask<object?>
object valueTask = invocation(message, messageContext)!;
object? typelessValueTask = typeEraseAwait.Invoke(null, [valueTask]);
Debug.Assert(typelessValueTask is ValueTask<object?>, "Expected ValueTask<object?> after type erasure.");
return await ((ValueTask<object?>)typelessValueTask).ConfigureAwait(false);
};
}
else
{
throw new InvalidOperationException($"Method {methodInfo.Name} must return a ValueTask or ValueTask<T>");
}
this.Invocation = getResultAsync;
}
/// <summary>
/// Invokes the handler method asynchronously with the provided message and context.
/// </summary>
/// <param name="obj">The message to be passed as the first argument to the handler.</param>
/// <param name="messageContext">The contextual information associated with the message.</param>
/// <returns>A ValueTask representing the asynchronous operation, which yields the handler's result.</returns>
public async ValueTask<object?> InvokeAsync(object? obj, MessageContext messageContext)
{
try
{
return await this.Invocation.Invoke(obj, messageContext).ConfigureAwait(false);
}
catch (TargetInvocationException ex)
{
// Unwrap the exception to get the original exception thrown by the handler method.
Exception? innerException = ex.InnerException;
if (innerException != null)
{
throw innerException;
}
throw;
}
}
/// <summary>
/// Awaits a generic ValueTask and returns its result as an object.
/// This method is used to convert a ValueTask{T} to ValueTask{object?}.
/// </summary>
/// <typeparam name="T">The type of the result contained in the ValueTask.</typeparam>
/// <param name="vt">The ValueTask to be awaited.</param>
/// <returns>A ValueTask containing the result as an object.</returns>
private static async ValueTask<object?> TypeEraseAwaitAsync<T>(ValueTask<T> vt)
{
return await vt.ConfigureAwait(false);
}
}
@@ -9,23 +9,22 @@ using System.Threading.Tasks;
namespace Microsoft.Extensions.AI.Agents.Runtime;
/// <summary>
/// Defines the runtime environment for agents, managing message sending, subscriptions, agent resolution, and state persistence.
/// Defines the runtime environment for actors, managing message sending, subscriptions, actor resolution, and state persistence,
/// all in support of agent-based architectures.
/// </summary>
public interface IAgentRuntime : ISaveState
{
/// <summary>
/// Sends a message to an agent and gets a response.
/// This method should be used to communicate directly with an agent.
/// Sends a message to an actor and gets a response.
/// This method should be used to communicate directly with an actor.
/// </summary>
/// <param name="message">The message to send.</param>
/// <param name="recipient">The agent to send the message to.</param>
/// <param name="sender">The agent sending the message. Should be <c>null</c> if sent from an external source.</param>
/// <param name="recipient">The actor to send the message to.</param>
/// <param name="sender">The actor sending the message. Should be <c>null</c> if sent from an external source.</param>
/// <param name="messageId">A unique identifier for the message. If <c>null</c>, a new ID will be generated.</param>
/// <param name="cancellationToken">A token to cancel the operation if needed.</param>
/// <returns>A task representing the asynchronous operation, returning the response from the agent.</returns>
/// <exception cref="CantHandleException">Thrown if the recipient cannot handle the message.</exception>
/// <exception cref="UndeliverableException">Thrown if the message cannot be delivered.</exception>
ValueTask<object?> SendMessageAsync(object message, AgentId recipient, AgentId? sender = null, string? messageId = null, CancellationToken cancellationToken = default);
/// <returns>A task representing the asynchronous operation, returning the response from the actor.</returns>
ValueTask<object?> SendMessageAsync(object message, ActorId recipient, ActorId? sender = null, string? messageId = null, CancellationToken cancellationToken = default);
/// <summary>
/// Publishes a message to all agents subscribed to the given topic.
@@ -33,90 +32,99 @@ public interface IAgentRuntime : ISaveState
/// </summary>
/// <param name="message">The message to publish.</param>
/// <param name="topic">The topic to publish the message to.</param>
/// <param name="sender">The agent sending the message. Defaults to <c>null</c>.</param>
/// <param name="sender">The actor sending the message. Defaults to <c>null</c>.</param>
/// <param name="messageId">A unique message ID. If <c>null</c>, a new one will be generated.</param>
/// <param name="cancellationToken">A token to cancel the operation if needed.</param>
/// <returns>A task representing the asynchronous operation.</returns>
/// <exception cref="UndeliverableException">Thrown if the message cannot be delivered.</exception>
ValueTask PublishMessageAsync(object message, TopicId topic, AgentId? sender = null, string? messageId = null, CancellationToken cancellationToken = default);
ValueTask PublishMessageAsync(object message, TopicId topic, ActorId? sender = null, string? messageId = null, CancellationToken cancellationToken = default);
/// <summary>
/// Retrieves an agent by its unique identifier.
/// Retrieves an actor by its unique identifier.
/// </summary>
/// <param name="agentId">The unique identifier of the agent.</param>
/// <param name="lazy">If <c>true</c>, the agent is fetched lazily.</param>
/// <returns>A task representing the asynchronous operation, returning the agent's ID.</returns>
ValueTask<AgentId> GetAgentAsync(AgentId agentId, bool lazy = true/*, CancellationToken? = default*/);
/// <param name="actorId">The unique identifier of the actor.</param>
/// <param name="lazy">If <c>true</c>, the actor is fetched lazily.</param>
/// <param name="cancellationToken">A token to cancel the operation if needed.</param>
/// <returns>A task representing the asynchronous operation, returning the actor's ID.</returns>
ValueTask<ActorId> GetActorAsync(ActorId actorId, bool lazy = true, CancellationToken cancellationToken = default);
/// <summary>
/// Retrieves an agent by its type.
/// Retrieves an actor by its type.
/// </summary>
/// <param name="agentType">The type of the agent.</param>
/// <param name="key">An optional key to specify variations of the agent. Defaults to "default".</param>
/// <param name="lazy">If <c>true</c>, the agent is fetched lazily.</param>
/// <returns>A task representing the asynchronous operation, returning the agent's ID.</returns>
ValueTask<AgentId> GetAgentAsync(AgentType agentType, string key = "default", bool lazy = true/*, CancellationToken? = default*/);
/// <param name="actorType">The type of the actor.</param>
/// <param name="key">An optional key to specify variations of the actor. Defaults to "default".</param>
/// <param name="lazy">If <c>true</c>, the actor is fetched lazily.</param>
/// <param name="cancellationToken">A token to cancel the operation if needed.</param>
/// <returns>A task representing the asynchronous operation, returning the actor's ID.</returns>
ValueTask<ActorId> GetActorAsync(ActorType actorType, string key = "default", bool lazy = true, CancellationToken cancellationToken = default);
/// <summary>
/// Retrieves an agent by its string representation.
/// Retrieves an actor by its string representation.
/// </summary>
/// <param name="agent">The string representation of the agent.</param>
/// <param name="key">An optional key to specify variations of the agent. Defaults to "default".</param>
/// <param name="lazy">If <c>true</c>, the agent is fetched lazily.</param>
/// <returns>A task representing the asynchronous operation, returning the agent's ID.</returns>
ValueTask<AgentId> GetAgentAsync(string agent, string key = "default", bool lazy = true/*, CancellationToken? = default*/);
/// <param name="actor">The string representation of the actor.</param>
/// <param name="key">An optional key to specify variations of the actor. Defaults to "default".</param>
/// <param name="lazy">If <c>true</c>, the actor is fetched lazily.</param>
/// <param name="cancellationToken">A token to cancel the operation if needed.</param>
/// <returns>A task representing the asynchronous operation, returning the actor's ID.</returns>
ValueTask<ActorId> GetActorAsync(string actor, string key = "default", bool lazy = true, CancellationToken cancellationToken = default);
/// <summary>
/// Saves the state of an agent.
/// Saves the state of an actor.
/// The result must be JSON serializable.
/// </summary>
/// <param name="agentId">The ID of the agent whose state is being saved.</param>
/// <param name="actorId">The ID of the actor whose state is being saved.</param>
/// <param name="cancellationToken">A token to cancel the operation if needed.</param>
/// <returns>A task representing the asynchronous operation, returning a dictionary of the saved state.</returns>
ValueTask<JsonElement> SaveAgentStateAsync(AgentId agentId/*, CancellationToken? cancellationToken = default*/);
ValueTask<JsonElement> SaveActorStateAsync(ActorId actorId, CancellationToken cancellationToken = default);
/// <summary>
/// Loads the saved state into an agent.
/// Loads the saved state into an actor.
/// </summary>
/// <param name="agentId">The ID of the agent whose state is being restored.</param>
/// <param name="actorId">The ID of the actor whose state is being restored.</param>
/// <param name="state">The state dictionary to restore.</param>
/// <param name="cancellationToken">A token to cancel the operation if needed.</param>
/// <returns>A task representing the asynchronous operation.</returns>
ValueTask LoadAgentStateAsync(AgentId agentId, JsonElement state/*, CancellationToken? cancellationToken = default*/);
ValueTask LoadActorStateAsync(ActorId actorId, JsonElement state, CancellationToken cancellationToken = default);
/// <summary>
/// Retrieves metadata for an agent.
/// Retrieves metadata for an actor.
/// </summary>
/// <param name="agentId">The ID of the agent.</param>
/// <returns>A task representing the asynchronous operation, returning the agent's metadata.</returns>
ValueTask<AgentMetadata> GetAgentMetadataAsync(AgentId agentId/*, CancellationToken? cancellationToken = default*/);
/// <param name="actorId">The ID of the actor.</param>
/// <param name="cancellationToken">A token to cancel the operation if needed.</param>
/// <returns>A task representing the asynchronous operation, returning the actor's metadata.</returns>
ValueTask<ActorMetadata> GetActorMetadataAsync(ActorId actorId, CancellationToken cancellationToken = default);
/// <summary>
/// Adds a new subscription for the runtime to handle when processing published messages.
/// </summary>
/// <param name="subscription">The subscription to add.</param>
/// <param name="cancellationToken">A token to cancel the operation if needed.</param>
/// <returns>A task representing the asynchronous operation.</returns>
ValueTask AddSubscriptionAsync(ISubscriptionDefinition subscription/*, CancellationToken? cancellationToken = default*/);
ValueTask AddSubscriptionAsync(ISubscriptionDefinition subscription, CancellationToken cancellationToken = default);
/// <summary>
/// Removes a subscription from the runtime.
/// </summary>
/// <param name="subscriptionId">The unique identifier of the subscription to remove.</param>
/// <param name="cancellationToken">A token to cancel the operation if needed.</param>
/// <returns>A task representing the asynchronous operation.</returns>
/// <exception cref="KeyNotFoundException">Thrown if the subscription does not exist.</exception>
ValueTask RemoveSubscriptionAsync(string subscriptionId/*, CancellationToken? cancellationToken = default*/);
ValueTask RemoveSubscriptionAsync(string subscriptionId, CancellationToken cancellationToken = default);
/// <summary>
/// Registers an agent factory with the runtime, associating it with a specific agent type.
/// Registers an actor factory with the runtime, associating it with a specific actor type.
/// The type must be unique.
/// </summary>
/// <param name="type">The agent type to associate with the factory.</param>
/// <param name="factoryFunc">A function that asynchronously creates the agent instance.</param>
/// <returns>A task representing the asynchronous operation, returning the registered <see cref="AgentType"/>.</returns>
ValueTask<AgentType> RegisterAgentFactoryAsync(AgentType type, Func<AgentId, IAgentRuntime, ValueTask<IHostableAgent>> factoryFunc);
/// <param name="type">The actor type to associate with the factory.</param>
/// <param name="factoryFunc">A function that asynchronously creates the actor instance.</param>
/// <param name="cancellationToken">A token to cancel the operation if needed.</param>
/// <returns>A task representing the asynchronous operation, returning the registered <see cref="ActorType"/>.</returns>
ValueTask<ActorType> RegisterActorFactoryAsync(ActorType type, Func<ActorId, IAgentRuntime, ValueTask<IRuntimeActor>> factoryFunc, CancellationToken cancellationToken = default);
/// <summary>
/// Attempts to retrieve an <see cref="AgentProxy"/> for the specified agent.
/// Attempts to retrieve an <see cref="IdProxyActor"/> for the specified actor.
/// </summary>
/// <param name="agentId">The ID of the agent.</param>
/// <returns>A task representing the asynchronous operation, returning an <see cref="AgentProxy"/> if successful.</returns>
ValueTask<AgentProxy> TryGetAgentProxyAsync(AgentId agentId);
/// <param name="actorId">The ID of the actor.</param>
/// <param name="cancellationToken">A token to cancel the operation if needed.</param>
/// <returns>A task representing the asynchronous operation, returning an <see cref="IdProxyActor"/> if successful.</returns>
ValueTask<IdProxyActor?> TryGetActorProxyAsync(ActorId actorId, CancellationToken cancellationToken = default);
}
@@ -1,36 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Threading.Tasks;
namespace Microsoft.Extensions.AI.Agents.Runtime;
/// <summary>
/// Defines a handler interface for processing items of type <typeparamref name="T"/>.
/// </summary>
/// <typeparam name="T">The type of item to be handled.</typeparam>
public interface IHandle<in T>
{
/// <summary>
/// Handles the specified item asynchronously.
/// </summary>
/// <param name="item">The item to be handled.</param>
/// <param name="messageContext">The context of the message being handled.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
ValueTask HandleAsync(T item, MessageContext messageContext);
}
/// <summary>
/// Defines a handler interface for processing items of type <typeparamref name="TIn"/> and <typeparamref name="TOut"/>.
/// </summary>
/// <typeparam name="TIn">The input type</typeparam>
/// <typeparam name="TOut">The output type</typeparam>
public interface IHandle<in TIn, TOut>
{
/// <summary>
/// Handles the specified item asynchronously.
/// </summary>
/// <param name="item">The item to be handled.</param>
/// <param name="messageContext">The context of the message being handled.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
ValueTask<TOut> HandleAsync(TIn item, MessageContext messageContext);
}
@@ -1,17 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Threading.Tasks;
namespace Microsoft.Extensions.AI.Agents.Runtime;
/// <summary>
/// Represents an agent that can be explicitly hosted and closed when the runtime shuts down.
/// </summary>
public interface IHostableAgent : IAgent
{
/// <summary>
/// Called when the runtime is closing.
/// </summary>
/// <returns>A task representing the asynchronous operation.</returns>
ValueTask CloseAsync();
}
@@ -1,36 +1,37 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Threading;
using System.Threading.Tasks;
namespace Microsoft.Extensions.AI.Agents.Runtime;
/// <summary>
/// Represents an agent within the runtime that can process messages, maintain state, and be closed when no longer needed.
/// Represents an actor within the runtime that can process messages, maintain state, and be closed when no longer needed.
/// </summary>
public interface IAgent : ISaveState
public interface IRuntimeActor : ISaveState
{
/// <summary>
/// Gets the unique identifier of the agent.
/// Gets the unique identifier of the actor.
/// </summary>
AgentId Id { get; }
ActorId Id { get; }
/// <summary>
/// Gets metadata associated with the agent.
/// Gets metadata associated with the actor.
/// </summary>
AgentMetadata Metadata { get; }
ActorMetadata Metadata { get; }
/// <summary>
/// Handles an incoming message for the agent.
/// This should only be called by the runtime, not by other agents.
/// Handles an incoming message for the actor.
/// This should only be called by the runtime, not by other actors.
/// </summary>
/// <param name="message">The received message. The type should match one of the expected subscription types.</param>
/// <param name="messageContext">The context of the message, providing additional metadata.</param>
/// <param name="cancellationToken">A token to cancel the operation if needed.</param>
/// <returns>
/// A task representing the asynchronous operation, returning a response to the message.
/// The response can be <c>null</c> if no reply is necessary.
/// </returns>
/// <exception cref="OperationCanceledException">Thrown if the message was cancelled.</exception>
/// <exception cref="CantHandleException">Thrown if the agent cannot handle the message.</exception>
ValueTask<object?> OnMessageAsync(object message, MessageContext messageContext); // TODO: How do we express this properly in .NET?
ValueTask<object?> OnMessageAsync(object message, MessageContext messageContext, CancellationToken cancellationToken = default);
}
@@ -1,25 +1,30 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
namespace Microsoft.Extensions.AI.Agents.Runtime;
// TODO: Why is this interface needed? It's inherited by IAgentRuntime and IRuntimeActor.
// Is the former needed (does IAgentRuntime need to not only persist every actor but do so via
// this interface)? If not, these methods could be moved to IRuntimeActor.
/// <summary>
/// Defines a contract for saving and loading the state of an object.
/// The state must be JSON serializable.
/// Defines a contract for saving and loading the state of an object as JSON.
/// </summary>
public interface ISaveState
{
/// <summary>
/// Saves the current state of the object.
/// </summary>
/// <param name="cancellationToken">A token to cancel the operation if needed.</param>
/// <returns>
/// A task representing the asynchronous operation, returning a dictionary
/// containing the saved state. The structure of the state is implementation-defined
/// but must be JSON serializable.
/// </returns>
ValueTask<JsonElement> SaveStateAsync();
ValueTask<JsonElement> SaveStateAsync(CancellationToken cancellationToken = default);
/// <summary>
/// Loads a previously saved state into the object.
@@ -28,6 +33,7 @@ public interface ISaveState
/// A dictionary representing the saved state. The structure of the state
/// is implementation-defined but must be JSON serializable.
/// </param>
/// <param name="cancellationToken">A token to cancel the operation if needed.</param>
/// <returns>A task representing the asynchronous operation.</returns>
ValueTask LoadStateAsync(JsonElement state);
ValueTask LoadStateAsync(JsonElement state, CancellationToken cancellationToken = default);
}
@@ -5,7 +5,7 @@ using System.Diagnostics.CodeAnalysis;
namespace Microsoft.Extensions.AI.Agents.Runtime;
/// <summary>
/// Defines a subscription that matches topics and maps them to agents.
/// Defines a subscription that matches topics and maps them to actors.
/// </summary>
public interface ISubscriptionDefinition
{
@@ -42,10 +42,10 @@ public interface ISubscriptionDefinition
bool Matches(TopicId topic);
/// <summary>
/// Maps a <see cref="TopicId"/> to an <see cref="AgentId"/>.
/// Maps a <see cref="TopicId"/> to an <see cref="ActorId"/>.
/// Should only be called if <see cref="Matches"/> returns <c>true</c>.
/// </summary>
/// <param name="topic">The topic to map.</param>
/// <returns>The <see cref="AgentId"/> that should handle the topic.</returns>
AgentId MapToAgent(TopicId topic);
/// <returns>The <see cref="ActorId"/> that should handle the topic.</returns>
ActorId MapToActor(TopicId topic);
}
@@ -0,0 +1,53 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
namespace Microsoft.Extensions.AI.Agents.Runtime;
/// <summary>
/// Provides an actor proxy that allows you to use an <see cref="ActorId"/> in place of its associated <see cref="IRuntimeActor"/>.
/// </summary>
public sealed class IdProxyActor : IRuntimeActor
{
/// <summary>The runtime instance used to interact with actors.</summary>
private readonly IAgentRuntime _runtime;
/// <summary>The metadata for the actor, lazy-loaded.</summary>
private ActorMetadata? _metadata;
/// <summary>
/// Initializes a new instance of the <see cref="IdProxyActor"/> class.
/// </summary>
public IdProxyActor(IAgentRuntime runtime, ActorId actorId)
{
this.Id = actorId;
this._runtime = runtime;
}
/// <inheritdoc />
public ActorId Id { get; }
/// <inheritdoc />
public ActorMetadata Metadata =>
this._metadata ??=
#pragma warning disable VSTHRD002 // Avoid problematic synchronous waits
this._runtime.GetActorMetadataAsync(this.Id).AsTask().GetAwaiter().GetResult();
#pragma warning restore VSTHRD002
/// <inheritdoc />
public ValueTask<object?> SendMessageAsync(object message, ActorId sender, string? messageId = null, CancellationToken cancellationToken = default) =>
this._runtime.SendMessageAsync(message, this.Id, sender, messageId, cancellationToken);
/// <inheritdoc />
public ValueTask LoadStateAsync(JsonElement state, CancellationToken cancellationToken = default) =>
this._runtime.LoadActorStateAsync(this.Id, state, cancellationToken);
/// <inheritdoc />
public ValueTask<JsonElement> SaveStateAsync(CancellationToken cancellationToken = default) =>
this._runtime.SaveActorStateAsync(this.Id, cancellationToken);
/// <inheritdoc />
ValueTask<object?> IRuntimeActor.OnMessageAsync(object message, MessageContext messageContext, CancellationToken cancellationToken) =>
new((object?)null);
}
@@ -1,52 +1,31 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Text.RegularExpressions;
using System.Diagnostics.CodeAnalysis;
namespace Microsoft.Extensions.AI.Agents.Runtime;
/// <summary>
/// Provides helper methods for parsing key-value string representations.
/// </summary>
internal static class KeyValueParserExtensions
internal static class KeyValueParser
{
/// <summary>
/// The regular expression pattern used to match key-value pairs in the format "key/value".
/// </summary>
private const string KVPairPattern = @"^(?<key>\w+)/(?<value>\w+)$";
/// <summary>
/// The compiled regex used for extracting key-value pairs from a string.
/// </summary>
private static readonly Regex KVPairRegex = new(KVPairPattern, RegexOptions.Compiled);
/// <summary>
/// Parses a string in the format "key/value" into a tuple containing the key and value.
/// </summary>
/// <param name="inputPair">The input string containing a key-value pair.</param>
/// <param name="keyName">The expected name of the key component.</param>
/// <param name="valueName">The expected name of the value component.</param>
/// <returns>A tuple containing the extracted key and value.</returns>
/// <exception cref="FormatException">
/// Thrown if the input string does not match the expected "key/value" format.
/// </exception>
/// <example>
/// Example usage:
/// <code>
/// string input = "agent1/12345";
/// var result = input.ToKVPair("Type", "Key");
/// Console.WriteLine(result.Item1); // Outputs: agent1
/// Console.WriteLine(result.Item2); // Outputs: 12345
/// </code>
/// </example>
public static (string, string) ToKeyValuePair(this string inputPair, string keyName, string valueName)
public static bool TryParse(string input, [NotNullWhen(true)] out string? key, [NotNullWhen(true)] out string? value)
{
Match match = KVPairRegex.Match(inputPair);
if (match.Success)
if (!string.IsNullOrEmpty(input))
{
return (match.Groups["key"].Value, match.Groups["value"].Value);
int separatorIndex = input.IndexOf('/');
if (separatorIndex >= 0)
{
key = input.Substring(0, separatorIndex);
value = input.Substring(separatorIndex + 1);
return true;
}
}
throw new FormatException($"Invalid key-value pair format: {inputPair}; expecting \"{{{keyName}}}/{{{valueName}}}\"");
key = value = null;
return false;
}
}
@@ -7,32 +7,36 @@ namespace Microsoft.Extensions.AI.Agents.Runtime;
/// <summary>
/// Represents the context of a message being sent within the agent runtime.
/// This includes metadata such as the sender, topic, RPC status, and cancellation handling.
/// </summary>
public class MessageContext(string messageId, CancellationToken cancellationToken)
/// <remarks>
/// This includes metadata such as the sender, topic, ahd RPC status.
/// </remarks>
public sealed class MessageContext
{
/// <summary>
/// Initializes a new instance of the <see cref="MessageContext"/> class.
/// </summary>
public MessageContext(CancellationToken cancellation) : this(Guid.NewGuid().ToString(), cancellation)
{ }
private string? _messageId;
/// <summary>
/// Gets or sets the unique identifier for this message.
/// </summary>
public string MessageId { get; } = messageId;
public string MessageId
{
get => this._messageId ?? Interlocked.CompareExchange(ref this._messageId, Guid.NewGuid().ToString(), null) ?? this._messageId;
set
{
if (string.IsNullOrEmpty(value))
{
throw new ArgumentException("MessageId cannot be null or empty.", nameof(value));
}
/// <summary>
/// Gets or sets the cancellation token associated with this message.
/// This can be used to cancel the operation if necessary.
/// </summary>
public CancellationToken CancellationToken { get; } = cancellationToken;
this._messageId = value;
}
}
/// <summary>
/// Gets or sets the sender of the message.
/// If <c>null</c>, the sender is unspecified.
/// </summary>
public AgentId? Sender { get; set; }
public ActorId? Sender { get; set; }
/// <summary>
/// Gets or sets the topic associated with the message.
@@ -5,15 +5,19 @@
<TargetFrameworks Condition="'$(Configuration)' == 'Debug'">$(ProjectsDebugTargetFrameworks)</TargetFrameworks>
<NoWarn>$(NoWarn);IDE1006;IDE0130</NoWarn>
<VersionSuffix>alpha</VersionSuffix>
<IsAotCompatible>false</IsAotCompatible> <!-- TODO: Fix this -->
</PropertyGroup>
<PropertyGroup>
<InjectDiagnosticAttributesOnLegacy>true</InjectDiagnosticAttributesOnLegacy>
<InjectTrimAttributesOnLegacy>true</InjectTrimAttributesOnLegacy>
</PropertyGroup>
<Import Project="$(RepoRoot)/dotnet/nuget/nuget-package.props" />
<ItemGroup>
<None Include="IRuntimeActor.cs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
</ItemGroup>
@@ -0,0 +1,188 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
namespace Microsoft.Extensions.AI.Agents.Runtime;
/// <summary>
/// Provides a base implementation of <see cref="IRuntimeActor"/>.
/// </summary>
public abstract class RuntimeActor : IRuntimeActor
{
private static readonly JsonElement s_emptyElement = JsonDocument.Parse("{}").RootElement;
/// <summary>
/// The activity source for tracing.
/// </summary>
public static readonly ActivitySource TraceSource = new($"{typeof(IRuntimeActor).Namespace}");
private readonly Dictionary<Type, HandlerInvoker> _handlerInvokers = [];
private readonly IAgentRuntime _runtime;
private delegate ValueTask<object?> HandlerInvoker(object? message, MessageContext messageContext, CancellationToken cancellationToken);
/// <summary>
/// Provides logging capabilities used for diagnostic and operational information.
/// </summary>
protected internal ILogger Logger { get; }
/// <summary>
/// Gets the unique identifier of the actor.
/// </summary>
public ActorId Id { get; }
/// <summary>
/// Gets the metadata of the actor.
/// </summary>
public ActorMetadata Metadata { get; }
/// <summary>
/// Initializes a new instance of the RuntimeActor class with the specified identifier, runtime, description, and optional logger.
/// </summary>
/// <param name="id">The unique identifier of the actor.</param>
/// <param name="runtime">The runtime environment in which the actor operates.</param>
/// <param name="description">A brief description of the actor's purpose.</param>
/// <param name="logger">An optional logger for recording diagnostic information.</param>
protected RuntimeActor(
ActorId id,
IAgentRuntime runtime,
string description,
ILogger? logger = null)
{
this.Id = id;
this._runtime = runtime;
this.Logger = logger ?? NullLogger.Instance;
this.Metadata = new ActorMetadata(this.Id.Type, this.Id.Key, description);
}
/// <summary>Registers a handler for <typeparamref name="TInput"/>.</summary>
/// <typeparam name="TInput">The type of the input message for the handler.</typeparam>
/// <param name="messageHandler">The handler function that processes the message.</param>
/// <exception cref="InvalidOperationException">Thrown when a handler for the specified type is already registered.</exception>
/// <remarks>
/// The base implementation of <see cref="OnMessageAsync"/> will use these registered handlers to process incoming messages.
/// </remarks>
protected void RegisterMessageHandler<TInput>(Func<TInput, MessageContext, CancellationToken, ValueTask> messageHandler)
{
if (messageHandler is null)
{
throw new ArgumentNullException(nameof(messageHandler));
}
if (this._handlerInvokers.ContainsKey(typeof(TInput)))
{
throw new InvalidOperationException($"A handler for type {typeof(TInput)} is already registered.");
}
this._handlerInvokers.Add(
typeof(TInput),
async (message, messageContext, cancellationToken) =>
{
await messageHandler((TInput)message!, messageContext, cancellationToken).ConfigureAwait(false);
return null; // No return value for void handlers
});
}
/// <summary>Registers a handler for <typeparamref name="TInput"/> that produces a <typeparamref name="TOutput"/>.</summary>
/// <typeparam name="TInput">The type of the input message for the handler.</typeparam>
/// <typeparam name="TOutput">The type of the output message for the handler.</typeparam>
/// <param name="messageHandler">The handler function that processes the message.</param>
/// <exception cref="InvalidOperationException">Thrown when a handler for the specified type is already registered.</exception>
/// <remarks>
/// The base implementation of <see cref="OnMessageAsync"/> will use these registered handlers to process incoming messages.
/// </remarks>
protected void RegisterMessageHandler<TInput, TOutput>(Func<TInput, MessageContext, CancellationToken, ValueTask<TOutput>> messageHandler)
{
if (messageHandler is null)
{
throw new ArgumentNullException(nameof(messageHandler));
}
if (this._handlerInvokers.ContainsKey(typeof(TInput)))
{
throw new InvalidOperationException($"A handler for type {typeof(TInput)} is already registered.");
}
this._handlerInvokers.Add(
typeof(TInput),
async (message, messageContext, cancellationToken) =>
{
TOutput? result = await messageHandler((TInput)message!, messageContext, cancellationToken).ConfigureAwait(false);
return (object?)result;
});
}
/// <summary>
/// Handles an incoming message by determining its type and invoking the corresponding handler method if available.
/// </summary>
/// <param name="message">The message object to be handled.</param>
/// <param name="messageContext">The context associated with the message.</param>
/// <param name="cancellationToken">A token used to cancel the operation if needed.</param>
/// <returns>A ValueTask that represents the asynchronous operation, containing the response object or null.</returns>
public ValueTask<object?> OnMessageAsync(object message, MessageContext messageContext, CancellationToken cancellationToken = default)
{
// Get the handler for the message type, and invoke it, if it exists.
if (message is not null && this._handlerInvokers.TryGetValue(message.GetType(), out HandlerInvoker? handlerInvoker))
{
return handlerInvoker(message, messageContext, cancellationToken);
}
return new((object?)null);
}
/// <inheritdoc/>
public virtual ValueTask<JsonElement> SaveStateAsync(CancellationToken cancellationToken = default) =>
new(s_emptyElement);
/// <inheritdoc/>
public virtual ValueTask LoadStateAsync(JsonElement state, CancellationToken cancellationToken = default) =>
default;
/// <summary>
/// Sends a message to a specified recipient actor through the runtime.
/// </summary>
/// <param name="actor">The requested actor's type.</param>
/// <param name="cancellationToken">A token used to cancel the operation if needed.</param>
/// <returns>A ValueTask that represents the asynchronous operation, returning the response object or null.</returns>
protected async ValueTask<ActorId?> GetActorAsync(ActorType actor, CancellationToken cancellationToken = default)
{
try
{
return await this._runtime.GetActorAsync(actor, lazy: false, cancellationToken: cancellationToken).ConfigureAwait(false);
}
catch (InvalidOperationException)
{
return null;
}
}
/// <summary>
/// Sends a message to a specified recipient actor through the runtime.
/// </summary>
/// <param name="message">The message object to send.</param>
/// <param name="recipient">The recipient actor's identifier.</param>
/// <param name="messageId">An optional identifier for the message.</param>
/// <param name="cancellationToken">A token used to cancel the operation if needed.</param>
/// <returns>A ValueTask that represents the asynchronous operation, returning the response object or null.</returns>
protected ValueTask<object?> SendMessageAsync(object message, ActorId recipient, string? messageId = null, CancellationToken cancellationToken = default) =>
this._runtime.SendMessageAsync(message, recipient, sender: this.Id, messageId, cancellationToken);
/// <summary>
/// Publishes a message to all actors subscribed to a specific topic through the runtime.
/// </summary>
/// <param name="message">The message object to publish.</param>
/// <param name="topic">The topic identifier to which the message is published.</param>
/// <param name="messageId">An optional identifier for the message.</param>
/// <param name="cancellationToken">A token used to cancel the operation if needed.</param>
/// <returns>A ValueTask that represents the asynchronous publish operation.</returns>
protected ValueTask PublishMessageAsync(object message, TopicId topic, string? messageId = null, CancellationToken cancellationToken = default) =>
this._runtime.PublishMessageAsync(message, topic, sender: this.Id, messageId, cancellationToken);
}
@@ -2,147 +2,111 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Text.RegularExpressions;
namespace Microsoft.Extensions.AI.Agents.Runtime;
/// <summary>
/// Represents a topic identifier that defines the scope of a broadcast message.
/// Provides a topic identifier that defines the scope of a broadcast message.
/// </summary>
/// <remarks>
/// The agent runtime implements a publish-subscribe model through its broadcast API,
/// where messages must be published with a specific topic.
///
/// See the Python equivalent:
/// <see href="https://github.com/cloudevents/spec/blob/main/cloudevents/spec.md#type">CloudEvents Type Specification</see>.
/// </summary>
public struct TopicId : IEquatable<TopicId>
/// </remarks>
public readonly partial struct TopicId : IEquatable<TopicId>
{
/// <summary>
/// The default source value used when no source is explicitly provided.
/// </summary>
public const string DefaultSource = "default";
private const string TypePattern = @"^[\w-.:=]+$";
/// <summary>
/// The separator character for the string representation of the topic.
/// </summary>
public const string Separator = "/";
/// <summary>
/// Gets the type of the event that this <see cref="TopicId"/> represents.
/// This adheres to the CloudEvents specification.
///
/// Must match the pattern: <c>^[\w\-\.\:\=]+$</c>.
///
/// Learn more here:
/// <see href="https://github.com/cloudevents/spec/blob/main/cloudevents/spec.md#type">CloudEvents Type</see>.
/// </summary>
public string Type { get; }
/// <summary>
/// Gets the source that identifies the context in which an event happened.
/// This adheres to the CloudEvents specification.
///
/// Learn more here:
/// <see href="https://github.com/cloudevents/spec/blob/main/cloudevents/spec.md#source-1">CloudEvents Source</see>.
/// </summary>
public string Source { get; }
#if NET
[GeneratedRegex(TypePattern)]
private static partial Regex TypeRegex();
#else
private static Regex TypeRegex() => s_typeRegex;
private static readonly Regex s_typeRegex = new(TypePattern, RegexOptions.Compiled);
#endif
/// <summary>
/// Initializes a new instance of the <see cref="TopicId"/> struct.
/// </summary>
/// <param name="type">The type of the topic.</param>
/// <param name="source">The source of the event. Defaults to <see cref="DefaultSource"/> if not specified.</param>
public TopicId(string type, string source = DefaultSource)
/// <param name="type">The type of the topic. Must match the pattern: <c>^[\w-.:=]+$</c></param>
/// <param name="source">The source of the event.</param>
public TopicId(string type, string? source = null)
{
this.Type = type;
this.Source = source;
}
/// <summary>
/// Initializes a new instance of the <see cref="TopicId"/> struct from a tuple.
/// </summary>
/// <param name="kvPair">A tuple containing the topic type and source.</param>
public TopicId((string Type, string Source) kvPair) : this(kvPair.Type, kvPair.Source)
{
}
/// <summary>
/// Converts a string in the format "type/source" into a <see cref="TopicId"/>.
/// </summary>
/// <param name="maybeTopicId">The topic ID string.</param>
/// <returns>An instance of <see cref="TopicId"/>.</returns>
/// <exception cref="FormatException">Thrown when the string is not in the valid "type/source" format.</exception>
public static TopicId FromStr(string maybeTopicId) => new(maybeTopicId.ToKeyValuePair(nameof(Type), nameof(Source)));
/// <summary>
/// Returns the string representation of the <see cref="TopicId"/>.
/// </summary>
/// <returns>A string in the format "type/source".</returns>
public override readonly string ToString() => $"{this.Type}{Separator}{this.Source}";
/// <summary>
/// Determines whether the specified object is equal to the current <see cref="TopicId"/>.
/// </summary>
/// <param name="obj">The object to compare with the current instance.</param>
/// <returns><c>true</c> if the specified object is equal to the current <see cref="TopicId"/>; otherwise, <c>false</c>.</returns>
public override readonly bool Equals([NotNullWhen(true)] object? obj)
{
if (obj is TopicId other)
if (type is null)
{
return this.Type == other.Type && this.Source == other.Source;
throw new ArgumentNullException(nameof(type));
}
return false;
if (!TypeRegex().IsMatch(type))
{
throw new ArgumentException("Invalid type format.", nameof(type));
}
// TODO: What validation should be performed on source? The cited cloudevents spec suggests it should be a URI reference.
this.Type = type;
this.Source = source ?? "default";
}
/// <summary>
/// Determines whether the specified object is equal to the current <see cref="TopicId"/>.
/// Gets the type of the event that this <see cref="TopicId"/> represents.
/// </summary>
/// <param name="other">The object to compare with the current instance.</param>
/// <returns><c>true</c> if the specified object is equal to the current <see cref="TopicId"/>; otherwise, <c>false</c>.</returns>
public readonly bool Equals([NotNullWhen(true)] TopicId other)
{
return this.Type == other.Type && this.Source == other.Source;
}
/// <remarks>
/// This adheres to the CloudEvents specification.
/// <see href="https://github.com/cloudevents/spec/blob/main/cloudevents/spec.md#type">CloudEvents Type</see>.
/// </remarks>
public string Type { get; }
/// <summary>
/// Returns a hash code for this <see cref="TopicId"/>.
/// Gets the source that identifies the context in which an event happened.
/// </summary>
/// <returns>A hash code for the current instance.</returns>
public override readonly int GetHashCode()
{
return HashCode.Combine(this.Type, this.Source);
}
/// <remarks>
/// This adheres to the CloudEvents specification.
/// <see href="https://github.com/cloudevents/spec/blob/main/cloudevents/spec.md#source-1">CloudEvents Source</see>.
/// </remarks>
public string Source { get; }
/// <summary>
/// Explicitly converts a string to a <see cref="TopicId"/>.
/// Convert a string of the format "type/key" into an <see cref="TopicId"/>.
/// </summary>
/// <param name="id">The string representation of a topic ID.</param>
/// <param name="TopicId">The actor ID string.</param>
/// <returns>An instance of <see cref="TopicId"/>.</returns>
public static explicit operator TopicId(string id) => FromStr(id);
public static TopicId Parse(string TopicId)
{
if (!KeyValueParser.TryParse(TopicId, out string? type, out string? key))
{
throw new FormatException($"Invalid TopicId format: '{TopicId}'. Expected format is 'type/key'.");
}
return new TopicId(type, key);
}
/// <inheritdoc />
public override readonly string ToString() => $"{this.Type}/{this.Source}";
/// <inheritdoc />
public override readonly bool Equals([NotNullWhen(true)] object? obj) =>
obj is TopicId other && this.Equals(other);
/// <inheritdoc/>
public readonly bool Equals(TopicId other) =>
this.Type == other.Type && this.Source == other.Source;
/// <inheritdoc />
public override readonly int GetHashCode() =>
HashCode.Combine(this.Type, this.Source);
/// <inheritdoc />
public static bool operator ==(TopicId left, TopicId right) =>
left.Equals(right);
/// <inheritdoc />
public static bool operator !=(TopicId left, TopicId right) =>
!left.Equals(right);
// TODO: Implement < for wildcard matching (type, *)
// == => <
// Type == other.Type => <
/// <summary>
/// Determines whether the given <see cref="TopicId"/> matches another topic.
/// </summary>
/// <param name="other">The topic ID to compare against.</param>
/// <returns>
/// <c>true</c> if the topic types are equal; otherwise, <c>false</c>.
/// </returns>
public readonly bool IsWildcardMatch(TopicId other)
{
return this.Type == other.Type;
}
/// <inheritdoc/>
public static bool operator ==(TopicId left, TopicId right)
{
return left.Equals(right);
}
/// <inheritdoc/>
public static bool operator !=(TopicId left, TopicId right)
{
return !(left == right);
}
//public readonly bool IsWildcardMatch(TopicId other)
//{
// return this.Type == other.Type;
//}
}
@@ -6,8 +6,8 @@ using System.Diagnostics.CodeAnalysis;
namespace Microsoft.Extensions.AI.Agents.Runtime;
/// <summary>
/// This subscription matches on topics based on the exact type and maps to agents using the source of the topic as the agent key.
/// This subscription causes each source to have its own agent instance.
/// This subscription matches on topics based on the exact type and maps to actors using the source of the topic as the actor key.
/// This subscription causes each source to have its own actor instance.
/// </summary>
/// <remarks>
/// Example:
@@ -15,21 +15,21 @@ namespace Microsoft.Extensions.AI.Agents.Runtime;
/// var subscription = new TypeSubscription("t1", "a1");
/// </code>
/// In this case:
/// - A <see cref="TopicId"/> with type `"t1"` and source `"s1"` will be handled by an agent of type `"a1"` with key `"s1"`.
/// - A <see cref="TopicId"/> with type `"t1"` and source `"s2"` will be handled by an agent of type `"a1"` with key `"s2"`.
/// - A <see cref="TopicId"/> with type `"t1"` and source `"s1"` will be handled by an actor of type `"a1"` with key `"s1"`.
/// - A <see cref="TopicId"/> with type `"t1"` and source `"s2"` will be handled by an actor of type `"a1"` with key `"s2"`.
/// </remarks>
public class TypeSubscription : ISubscriptionDefinition
public sealed class TypeSubscription : ISubscriptionDefinition
{
/// <summary>
/// Initializes a new instance of the <see cref="TypeSubscription"/> class.
/// </summary>
/// <param name="topicType">The exact topic type to match against.</param>
/// <param name="agentType">Agent type to handle this subscription.</param>
/// <param name="actorType">Actor type to handle this subscription.</param>
/// <param name="id">Unique identifier for the subscription. If not provided, a new UUID will be generated.</param>
public TypeSubscription(string topicType, AgentType agentType, string? id = null)
public TypeSubscription(string topicType, ActorType actorType, string? id = null)
{
this.TopicType = topicType;
this.AgentType = agentType;
this.ActorType = actorType;
this.Id = id ?? Guid.NewGuid().ToString();
}
@@ -44,9 +44,9 @@ public class TypeSubscription : ISubscriptionDefinition
public string TopicType { get; }
/// <summary>
/// Gets the agent type that handles this subscription.
/// Gets the actor type that handles this subscription.
/// </summary>
public AgentType AgentType { get; }
public ActorType ActorType { get; }
/// <summary>
/// Checks if a given <see cref="TopicId"/> matches the subscription based on an exact type match.
@@ -59,19 +59,19 @@ public class TypeSubscription : ISubscriptionDefinition
}
/// <summary>
/// Maps a <see cref="TopicId"/> to an <see cref="AgentId"/>. Should only be called if <see cref="Matches"/> returns true.
/// Maps a <see cref="TopicId"/> to an <see cref="ActorId"/>. Should only be called if <see cref="Matches"/> returns true.
/// </summary>
/// <param name="topic">The topic to map.</param>
/// <returns>An <see cref="AgentId"/> representing the agent that should handle the topic.</returns>
/// <returns>An <see cref="ActorId"/> representing the actor that should handle the topic.</returns>
/// <exception cref="InvalidOperationException">Thrown if the topic does not match the subscription.</exception>
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(this.AgentType, topic.Source);
return new ActorId(this.ActorType, topic.Source);
}
/// <summary>
@@ -84,7 +84,7 @@ public class TypeSubscription : ISubscriptionDefinition
return
obj is TypeSubscription other &&
(this.Id == other.Id ||
(this.AgentType == other.AgentType &&
(this.ActorType == other.ActorType &&
this.TopicType == other.TopicType));
}
@@ -101,6 +101,6 @@ public class TypeSubscription : ISubscriptionDefinition
/// <returns>A hash code for this instance, suitable for use in hashing algorithms and data structures.</returns>
public override int GetHashCode()
{
return HashCode.Combine(this.Id, this.AgentType, this.TopicType);
return HashCode.Combine(this.Id, this.ActorType, this.TopicType);
}
}