mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Initial draft of actor runtime abstractions (#197)
* Initial draft of actor runtime abstractions
This commit is contained in:
@@ -2,6 +2,8 @@
|
||||
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.Runtime;
|
||||
|
||||
@@ -9,6 +11,7 @@ namespace Microsoft.Extensions.AI.Agents.Runtime;
|
||||
/// Provides a unique identifier for an actor instance within an agent runtime,
|
||||
/// serving as the "address" of the actor instance for receiving messages.
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(Converter))]
|
||||
public readonly struct ActorId : IEquatable<ActorId>
|
||||
{
|
||||
/// <summary>
|
||||
@@ -113,4 +116,28 @@ public readonly struct ActorId : IEquatable<ActorId>
|
||||
return true;
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// JSON converter for <see cref="ActorId"/>.
|
||||
/// </summary>
|
||||
public sealed class Converter : JsonConverter<ActorId>
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public override ActorId Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
if (reader.TokenType != JsonTokenType.String)
|
||||
{
|
||||
throw new JsonException("Expected string value for ActorId");
|
||||
}
|
||||
|
||||
string? actorIdString = reader.GetString() ?? throw new JsonException("ActorId cannot be null");
|
||||
return ActorId.Parse(actorIdString);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Write(Utf8JsonWriter writer, ActorId value, JsonSerializerOptions options)
|
||||
{
|
||||
writer.WriteStringValue(value.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// Source-generated JSON type information for use by all Actor abstractions.
|
||||
/// </summary>
|
||||
[JsonSourceGenerationOptions(
|
||||
JsonSerializerDefaults.Web,
|
||||
UseStringEnumConverter = true,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
WriteIndented = false)]
|
||||
[JsonSerializable(typeof(ActorMessage))]
|
||||
[JsonSerializable(typeof(ActorRequestMessage))]
|
||||
[JsonSerializable(typeof(ActorResponseMessage))]
|
||||
[JsonSerializable(typeof(ActorWriteOperation))]
|
||||
[JsonSerializable(typeof(SetValueOperation))]
|
||||
[JsonSerializable(typeof(RemoveKeyOperation))]
|
||||
[JsonSerializable(typeof(SendRequestOperation))]
|
||||
[JsonSerializable(typeof(UpdateRequestOperation))]
|
||||
[JsonSerializable(typeof(ActorReadOperation))]
|
||||
[JsonSerializable(typeof(ListKeysOperation))]
|
||||
[JsonSerializable(typeof(GetValueOperation))]
|
||||
[JsonSerializable(typeof(ActorReadResult))]
|
||||
[JsonSerializable(typeof(ListKeysResult))]
|
||||
[JsonSerializable(typeof(GetValueResult))]
|
||||
[JsonSerializable(typeof(ActorRequest))]
|
||||
[JsonSerializable(typeof(ActorRequestUpdate))]
|
||||
[JsonSerializable(typeof(ActorResponse))]
|
||||
[JsonSerializable(typeof(ActorId))]
|
||||
[JsonSerializable(typeof(RequestStatus))]
|
||||
[JsonSerializable(typeof(ActorWriteOperationBatch))]
|
||||
[JsonSerializable(typeof(ActorReadOperationBatch))]
|
||||
[JsonSerializable(typeof(ReadResponse))]
|
||||
[JsonSerializable(typeof(WriteResponse))]
|
||||
[JsonSerializable(typeof(ActorType))]
|
||||
[JsonSerializable(typeof(JsonElement))]
|
||||
internal sealed partial class ActorJsonContext : JsonSerializerContext;
|
||||
@@ -0,0 +1,39 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for all actor messages that can be sent between actors.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This abstract class serves as the foundation for all actor message types.
|
||||
/// Each concrete implementation represents a specific type of message,
|
||||
/// such as request messages or response messages.
|
||||
/// </remarks>
|
||||
//[JsonConverter(typeof(Converter))]
|
||||
[JsonPolymorphic(TypeDiscriminatorPropertyName = "type")]
|
||||
[JsonDerivedType(typeof(ActorRequestMessage), "request")]
|
||||
[JsonDerivedType(typeof(ActorResponseMessage), "response")]
|
||||
public abstract class ActorMessage
|
||||
{
|
||||
/// <summary>Prevent external derivations.</summary>
|
||||
private protected ActorMessage()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the type of the message.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public abstract ActorMessageType Type { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Additional properties that can be used to extend the message with custom data.
|
||||
/// </summary>
|
||||
[JsonExtensionData]
|
||||
public Dictionary<string, JsonElement>? ExtensionData { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// Specifies the type of actor message.
|
||||
/// </summary>
|
||||
public enum ActorMessageType
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a request message sent to an actor.
|
||||
/// </summary>
|
||||
[JsonStringEnumMemberName("request")]
|
||||
Request,
|
||||
|
||||
/// <summary>
|
||||
/// Represents a response message sent from an actor.
|
||||
/// </summary>
|
||||
[JsonStringEnumMemberName("response")]
|
||||
Response
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for write operations that modify an actor's messaging (inbox/outbox).
|
||||
/// </summary>
|
||||
public abstract class ActorMessageWriteOperation : ActorWriteOperation
|
||||
{
|
||||
/// <summary>Prevent external derivations.</summary>
|
||||
private protected ActorMessageWriteOperation()
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Text.Json.Serialization.Metadata;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for all actor read operations that can query actor state or messaging.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This abstract class serves as the foundation for all actor read operation types.
|
||||
/// Each concrete implementation represents a specific type of read operation,
|
||||
/// such as querying actor state or messaging information.
|
||||
/// </remarks>
|
||||
[JsonPolymorphic(TypeDiscriminatorPropertyName = "type")]
|
||||
[JsonDerivedType(typeof(ListKeysOperation), "list_keys")]
|
||||
[JsonDerivedType(typeof(GetValueOperation), "get_value")]
|
||||
public abstract class ActorReadOperation
|
||||
{
|
||||
/// <summary>Prevent external derivations.</summary>
|
||||
private protected ActorReadOperation()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the type of the read operation.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public abstract ActorReadOperationType Type { get; }
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a batch of read operations to be performed on an actor.
|
||||
/// </summary>
|
||||
/// <param name="operations">The collection of read operations to perform.</param>
|
||||
public class ActorReadOperationBatch(IReadOnlyList<ActorReadOperation> operations)
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the collection of read operations to perform.
|
||||
/// </summary>
|
||||
[JsonPropertyName("operations")]
|
||||
public IReadOnlyList<ActorReadOperation> Operations { get; } = operations;
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// Specifies the type of actor read operation.
|
||||
/// </summary>
|
||||
public enum ActorReadOperationType
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a list keys operation.
|
||||
/// </summary>
|
||||
[JsonStringEnumMemberName("list_keys")]
|
||||
ListKeys,
|
||||
|
||||
/// <summary>
|
||||
/// Represents a get value operation.
|
||||
/// </summary>
|
||||
[JsonStringEnumMemberName("get_value")]
|
||||
GetValue
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Text.Json.Serialization.Metadata;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for all actor read operation results.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This abstract class serves as the foundation for all actor read operation result types.
|
||||
/// Each concrete implementation represents a specific type of read operation result,
|
||||
/// such as listing keys or retrieving values from an actor's state.
|
||||
/// </remarks>
|
||||
[JsonPolymorphic(TypeDiscriminatorPropertyName = "type")]
|
||||
[JsonDerivedType(typeof(ListKeysResult), "list_keys")]
|
||||
[JsonDerivedType(typeof(GetValueOperation), "get_value")]
|
||||
public abstract class ActorReadResult
|
||||
{
|
||||
/// <summary>Prevent external derivations.</summary>
|
||||
private protected ActorReadResult()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the type of the read result operation.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public abstract ActorReadResultType Type { get; }
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// Specifies the type of actor read result operation.
|
||||
/// </summary>
|
||||
public enum ActorReadResultType
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a list keys operation result.
|
||||
/// </summary>
|
||||
[JsonStringEnumMemberName("list_keys")]
|
||||
ListKeys,
|
||||
|
||||
/// <summary>
|
||||
/// Represents a get value operation result.
|
||||
/// </summary>
|
||||
[JsonStringEnumMemberName("get_value")]
|
||||
GetValue
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a request to be sent to an actor.
|
||||
/// </summary>
|
||||
public class ActorRequest(ActorId actorId, string messageId, string method, JsonElement @params)
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the identifier of the target actor.
|
||||
/// </summary>
|
||||
[JsonPropertyName("actorId")]
|
||||
public ActorId ActorId { get; } = actorId;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the unique identifier for this request.
|
||||
/// </summary>
|
||||
[JsonPropertyName("messageId")]
|
||||
public string MessageId { get; } = messageId;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the method name to invoke on the actor.
|
||||
/// </summary>
|
||||
[JsonPropertyName("method")]
|
||||
public string Method { get; } = method;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the parameters for the method invocation.
|
||||
/// </summary>
|
||||
[JsonPropertyName("params")]
|
||||
public JsonElement Params { get; } = @params;
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for request messages sent to actors.
|
||||
/// </summary>
|
||||
public sealed class ActorRequestMessage(string MessageId) : ActorMessage
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public override ActorMessageType Type => ActorMessageType.Request;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the actor ID of the sender.
|
||||
/// </summary>
|
||||
[JsonPropertyName("sender")]
|
||||
public ActorId? SenderId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the unique identifier for the request.
|
||||
/// </summary>
|
||||
[JsonPropertyName("messageId")]
|
||||
public string MessageId { get; } = MessageId;
|
||||
|
||||
/// <summary>
|
||||
/// Name of the method to invoke.
|
||||
/// </summary>
|
||||
[JsonPropertyName("method")]
|
||||
public string? Method { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional parameters for the method invocation.
|
||||
/// </summary>
|
||||
[JsonPropertyName("params")]
|
||||
public JsonElement Params { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.Runtime;
|
||||
|
||||
// External (client) interface.
|
||||
|
||||
/// <summary>
|
||||
/// Represents an update to an actor request's status and data.
|
||||
/// </summary>
|
||||
public class ActorRequestUpdate(RequestStatus status, JsonElement data)
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the updated status of the request.
|
||||
/// </summary>
|
||||
[JsonPropertyName("status")]
|
||||
public RequestStatus Status { get; } = status;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the updated data associated with the request.
|
||||
/// </summary>
|
||||
[JsonPropertyName("data")]
|
||||
public JsonElement Data { get; } = data;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a response handle for an actor request, providing access to the result and status updates.
|
||||
/// </summary>
|
||||
public class ActorResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the identifier of the actor that is processing the request.
|
||||
/// </summary>
|
||||
[JsonPropertyName("actorId")]
|
||||
public ActorId ActorId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the unique identifier of the message/request.
|
||||
/// </summary>
|
||||
[JsonPropertyName("messageId")]
|
||||
public string? MessageId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the response data from the actor.
|
||||
/// </summary>
|
||||
[JsonPropertyName("data")]
|
||||
public JsonElement Data { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the current status of the request.
|
||||
/// </summary>
|
||||
[JsonPropertyName("status")]
|
||||
public RequestStatus Status { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a handle to an actor response, allowing retrieval of the response data and status updates.
|
||||
/// </summary>
|
||||
public abstract class ActorResponseHandle
|
||||
{
|
||||
/// <summary>
|
||||
/// Attempts to get the response from the request if it is immediately available.
|
||||
/// </summary>
|
||||
/// <param name="response">When this method returns <see langword="true"/>, contains the actor response; otherwise, <see langword="null"/>.</param>
|
||||
/// <returns><see langword="true"/> if the response is immediately available; otherwise, <see langword="false"/>.</returns>
|
||||
/// <remarks>
|
||||
/// This method does not block and returns immediately. If the request is still pending or processing,
|
||||
/// this method returns <see langword="false"/>.
|
||||
/// Use <see cref="GetResponseAsync(CancellationToken)"/> to wait asynchronously for the response to become available.
|
||||
/// </remarks>
|
||||
public abstract bool TryGetResponse([NotNullWhen(true)] out ActorResponse? response);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the response from the completed request.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">A token to cancel the wait operation.</param>
|
||||
/// <returns>A task that completes when the request is finished.</returns>
|
||||
public abstract ValueTask<ActorResponse> GetResponseAsync(CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Cancels the request if it is still pending.
|
||||
/// </summary>
|
||||
/// <returns>A task representing the cancellation operation.</returns>
|
||||
public abstract ValueTask CancelAsync(CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Watches for status and data updates to the request.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">A token to cancel the watch operation.</param>
|
||||
/// <returns>An asynchronous enumerable of request updates.</returns>
|
||||
public abstract IAsyncEnumerable<ActorRequestUpdate> WatchUpdatesAsync(CancellationToken cancellationToken);
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for response messages sent from actors.
|
||||
/// </summary>
|
||||
public sealed class ActorResponseMessage(string MessageId) : ActorMessage
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public override ActorMessageType Type => ActorMessageType.Response;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the actor ID of the sender.
|
||||
/// </summary>
|
||||
[JsonPropertyName("senderId")]
|
||||
public ActorId SenderId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the unique identifier for the request.
|
||||
/// </summary>
|
||||
[JsonPropertyName("messageId")]
|
||||
public string MessageId { get; } = MessageId;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the status of the request.
|
||||
/// </summary>
|
||||
[JsonPropertyName("status")]
|
||||
public RequestStatus Status { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the response data (result or error information).
|
||||
/// </summary>
|
||||
[JsonPropertyName("data")]
|
||||
public JsonElement Data { get; init; }
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for read operations that query an actor's internal state.
|
||||
/// </summary>
|
||||
public abstract class ActorStateReadOperation : ActorReadOperation
|
||||
{
|
||||
/// <summary>Prevent external derivations.</summary>
|
||||
private protected ActorStateReadOperation()
|
||||
{
|
||||
}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for write operations that modify an actor's internal state.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This abstract class serves as the foundation for all actor state write operation types.
|
||||
/// Each concrete implementation represents a specific type of state modification operation,
|
||||
/// such as setting or removing key-value pairs in an actor's state.
|
||||
/// </remarks>
|
||||
public abstract class ActorStateWriteOperation : ActorWriteOperation
|
||||
{
|
||||
/// <summary>Prevent external derivations.</summary>
|
||||
private protected ActorStateWriteOperation()
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.Runtime;
|
||||
@@ -8,6 +10,7 @@ namespace Microsoft.Extensions.AI.Agents.Runtime;
|
||||
/// <summary>
|
||||
/// Represents the type of an actor.
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(Converter))]
|
||||
public readonly partial struct ActorType : IEquatable<ActorType>
|
||||
{
|
||||
/// <summary>
|
||||
@@ -60,9 +63,33 @@ public readonly partial struct ActorType : IEquatable<ActorType>
|
||||
type is not null && TypeRegex().IsMatch(type);
|
||||
|
||||
#if NET
|
||||
[GeneratedRegex("^[a-zA-Z_][a-zA-Z_0-9]*$")]
|
||||
[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);
|
||||
private static Regex TypeRegex() => new("^[a-zA-Z_][a-zA-Z_:0-9:]*$", RegexOptions.Compiled);
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// JSON converter for <see cref="ActorType"/>.
|
||||
/// </summary>
|
||||
public sealed class Converter : JsonConverter<ActorType>
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public override ActorType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
if (reader.TokenType != JsonTokenType.String)
|
||||
{
|
||||
throw new JsonException("Expected string value for ActorType");
|
||||
}
|
||||
|
||||
string? actorTypeString = reader.GetString() ?? throw new JsonException("ActorType cannot be null");
|
||||
return new ActorType(actorTypeString);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Write(Utf8JsonWriter writer, ActorType value, JsonSerializerOptions options)
|
||||
{
|
||||
writer.WriteStringValue(value.Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for all actor write operations that can modify actor state or messaging.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This abstract class serves as the foundation for all actor write operation types.
|
||||
/// Each concrete implementation represents a specific type of write operation,
|
||||
/// such as modifying actor state or sending messages.
|
||||
/// </remarks>
|
||||
[JsonPolymorphic(TypeDiscriminatorPropertyName = "type")]
|
||||
[JsonDerivedType(typeof(SetValueOperation), "set_value")]
|
||||
[JsonDerivedType(typeof(RemoveKeyOperation), "remove_key")]
|
||||
[JsonDerivedType(typeof(UpdateRequestOperation), "update_request")]
|
||||
[JsonDerivedType(typeof(SendRequestOperation), "send_request")]
|
||||
public abstract class ActorWriteOperation
|
||||
{
|
||||
/// <summary>Prevent external derivations.</summary>
|
||||
private protected ActorWriteOperation()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the type of the write operation.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public abstract ActorWriteOperationType Type { get; }
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a batch of write operations to be performed atomically on an actor.
|
||||
/// </summary>
|
||||
/// <param name="eTag">The ETag for optimistic concurrency control.</param>
|
||||
/// <param name="operations">The collection of write operations to perform.</param>
|
||||
public class ActorWriteOperationBatch(string eTag, IReadOnlyCollection<ActorWriteOperation> operations)
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the collection of write operations to perform.
|
||||
/// </summary>
|
||||
[JsonPropertyName("operations")]
|
||||
public IReadOnlyCollection<ActorWriteOperation> Operations { get; } = operations;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the ETag for optimistic concurrency control.
|
||||
/// </summary>
|
||||
[JsonPropertyName("etag")]
|
||||
public string ETag { get; } = eTag;
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// Specifies the type of actor write operation.
|
||||
/// </summary>
|
||||
public enum ActorWriteOperationType
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a set key-value operation.
|
||||
/// </summary>
|
||||
[JsonStringEnumMemberName("set_value")]
|
||||
SetValue,
|
||||
|
||||
/// <summary>
|
||||
/// Represents a remove key operation.
|
||||
/// </summary>
|
||||
[JsonStringEnumMemberName("remove_key")]
|
||||
RemoveKey,
|
||||
|
||||
/// <summary>
|
||||
/// Represents a send request operation.
|
||||
/// </summary>
|
||||
[JsonStringEnumMemberName("send_request")]
|
||||
SendRequest,
|
||||
|
||||
/// <summary>
|
||||
/// Represents an update request operation.
|
||||
/// </summary>
|
||||
[JsonStringEnumMemberName("update_request")]
|
||||
UpdateRequest
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a request to read a value from the actor's state by its key.
|
||||
/// </summary>
|
||||
/// <param name="key">The key corresponding to the value to read from the actor's state.</param>
|
||||
public class GetValueOperation(string key) : ActorStateReadOperation
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the key corresponding to the value to read from the actor's state.
|
||||
/// </summary>
|
||||
[JsonPropertyName("key")]
|
||||
public string Key { get; } = key;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the type of the read operation.
|
||||
/// </summary>
|
||||
public override ActorReadOperationType Type => ActorReadOperationType.GetValue;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the result of a get value operation containing the retrieved value.
|
||||
/// </summary>
|
||||
/// <param name="value">The value retrieved from the actor's state, or null if not found.</param>
|
||||
public class GetValueResult(JsonElement? value) : ActorReadResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the value retrieved from the actor's state.
|
||||
/// </summary>
|
||||
[JsonPropertyName("value")]
|
||||
public JsonElement? Value { get; } = value;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the type of the read result operation.
|
||||
/// </summary>
|
||||
public override ActorReadResultType Type => ActorReadResultType.GetValue;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.Runtime;
|
||||
|
||||
// Implemented by the Agent Framework (eg, Agent, Orchestration, Process, etc)
|
||||
/// <summary>
|
||||
/// Represents an actor in the actor system that can process messages and maintain state.
|
||||
/// </summary>
|
||||
public interface IActor : IAsyncDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Runs the actor.
|
||||
/// When the value returned from this method completes, the actor is considered stopped.
|
||||
/// IActor is expected to call IActorContext.WatchMessagesAsync() to receive messages.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">A token to cancel the start operation.</param>
|
||||
/// <returns>A task representing the start operation.</returns>
|
||||
ValueTask RunAsync(CancellationToken cancellationToken);
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// Interface for sending requests to actors and managing responses.
|
||||
/// </summary>
|
||||
public interface IActorClient
|
||||
{
|
||||
/// <summary>
|
||||
/// Submits a request to an actor and gets a handle for the response.
|
||||
/// This method is idempotent: if the request is already in progress, it will return the existing response.
|
||||
/// </summary>
|
||||
/// <param name="request">The request to send to the actor.</param>
|
||||
/// <param name="cancellationToken">A token to cancel the operation.</param>
|
||||
/// <returns>A task representing the actor response handle.</returns>
|
||||
ValueTask<ActorResponseHandle> SendRequestAsync(ActorRequest request, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Gets an already-running request by its identifier.
|
||||
/// </summary>
|
||||
/// <param name="actorId">The identifier of the actor processing the request.</param>
|
||||
/// <param name="messageId">The unique identifier of the request message.</param>
|
||||
/// <param name="cancellationToken">A token to cancel the operation.</param>
|
||||
/// <returns>A task representing the actor response handle.</returns>
|
||||
ValueTask<ActorResponseHandle> GetResponseAsync(ActorId actorId, string messageId, CancellationToken cancellationToken);
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// Builder interface for configuring actor types in the runtime.
|
||||
/// </summary>
|
||||
public interface IActorRuntimeBuilder
|
||||
{
|
||||
/// <summary>
|
||||
/// Registers an actor type with its factory method.
|
||||
/// </summary>
|
||||
/// <param name="type">The actor type to register.</param>
|
||||
/// <param name="activator">The factory method to create instances of the actor.</param>
|
||||
void AddActorType(ActorType type, Func<IServiceProvider, IActorRuntimeContext, IActor> activator);
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// Provides the runtime context for an actor, enabling it to interact with the actor system.
|
||||
/// </summary>
|
||||
public interface IActorRuntimeContext
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the identifier of the actor.
|
||||
/// </summary>
|
||||
ActorId ActorId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Watches for incoming requests and responses in the actor's inbox and outbox.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">A token to cancel the watch operation.</param>
|
||||
/// <returns>An asynchronous enumerable of actor notifications.</returns>
|
||||
IAsyncEnumerable<ActorMessage> WatchMessagesAsync(CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Performs a batch of write operations atomically.
|
||||
/// </summary>
|
||||
/// <param name="operations">The batch of write operations to perform.</param>
|
||||
/// <param name="cancellationToken">A token to cancel the operation.</param>
|
||||
/// <returns>A task representing the write response.</returns>
|
||||
ValueTask<WriteResponse> WriteAsync(ActorWriteOperationBatch operations, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Performs a batch of read operations.
|
||||
/// </summary>
|
||||
/// <param name="operations">The batch of read operations to perform.</param>
|
||||
/// <param name="cancellationToken">A token to cancel the operation.</param>
|
||||
/// <returns>A task representing the read response.</returns>
|
||||
ValueTask<ReadResponse> ReadAsync(ActorReadOperationBatch operations, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Reports progress updates for streaming responses.
|
||||
/// The messageId must correspond to a non-terminated request in the actor's inbox (Status is Pending).
|
||||
/// </summary>
|
||||
/// <param name="messageId">The identifier of the message being updated.</param>
|
||||
/// <param name="sequenceNumber">The sequence number for ordering progress updates.</param>
|
||||
/// <param name="data">The progress data.</param>
|
||||
void OnProgressUpdate(string messageId, int sequenceNumber, JsonElement data);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// Interface for actor state storage operations, providing persistence for actor state data.
|
||||
/// </summary>
|
||||
public interface IActorStateStorage
|
||||
{
|
||||
/// <summary>
|
||||
/// Writes state changes to the actor's persistent storage.
|
||||
/// </summary>
|
||||
/// <param name="actorId">The identifier of the actor whose state is being modified.</param>
|
||||
/// <param name="operations">The collection of write operations to perform.</param>
|
||||
/// <param name="etag">The expected ETag for optimistic concurrency control.</param>
|
||||
/// <param name="cancellationToken">A token to cancel the operation.</param>
|
||||
/// <returns>A task representing the write response with success status and updated ETag.</returns>
|
||||
ValueTask<WriteResponse> WriteStateAsync(ActorId actorId, IReadOnlyCollection<ActorStateWriteOperation> operations, string etag, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Reads state data from the actor's persistent storage.
|
||||
/// </summary>
|
||||
/// <param name="actorId">The identifier of the actor whose state is being read.</param>
|
||||
/// <param name="operations">The collection of read operations to perform.</param>
|
||||
/// <param name="cancellationToken">A token to cancel the operation.</param>
|
||||
/// <returns>A task representing the read response with results and current ETag.</returns>
|
||||
ValueTask<ReadResponse> ReadStateAsync(ActorId actorId, IReadOnlyCollection<ActorStateReadOperation> operations, CancellationToken cancellationToken = default);
|
||||
}
|
||||
+384
@@ -0,0 +1,384 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// Provides an in-memory implementation of <see cref="IActorStateStorage"/> for testing and development scenarios.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This implementation stores all actor state in memory using concurrent dictionaries for thread safety.
|
||||
/// State is not persisted across application restarts and is lost when the application terminates.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The implementation provides optimistic concurrency control using ETags. Each write operation must
|
||||
/// provide the current ETag, and the operation will fail if the ETag has changed since the last read.
|
||||
/// This ensures that concurrent modifications to the same actor state are handled correctly.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Supported operations:
|
||||
/// <list type="bullet">
|
||||
/// <item><description><see cref="SetValueOperation"/> - Sets a key-value pair in the actor's state</description></item>
|
||||
/// <item><description><see cref="RemoveKeyOperation"/> - Removes a key from the actor's state</description></item>
|
||||
/// <item><description><see cref="GetValueOperation"/> - Retrieves a value by key from the actor's state</description></item>
|
||||
/// <item><description><see cref="ListKeysOperation"/> - Lists keys in the actor's state with optional prefix filtering</description></item>
|
||||
/// </list>
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// This implementation is suitable for:
|
||||
/// <list type="bullet">
|
||||
/// <item><description>Unit testing scenarios</description></item>
|
||||
/// <item><description>Development and prototyping</description></item>
|
||||
/// <item><description>Single-process applications where persistence is not required</description></item>
|
||||
/// </list>
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// For production scenarios requiring persistence, consider implementing a custom storage provider
|
||||
/// that uses a database or other persistent storage mechanism.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <example>
|
||||
/// <code>
|
||||
/// // Create storage instance
|
||||
/// var storage = new InMemoryActorStateStorage();
|
||||
/// var actorId = new ActorId("MyActor", "instance1");
|
||||
///
|
||||
/// // Write some state
|
||||
/// var writeOps = new List<ActorStateWriteOperation>
|
||||
/// {
|
||||
/// new SetValueOperation("name", JsonSerializer.SerializeToElement("John")),
|
||||
/// new SetValueOperation("age", JsonSerializer.SerializeToElement(30))
|
||||
/// };
|
||||
/// var writeResult = await storage.WriteStateAsync(actorId, writeOps, "0");
|
||||
///
|
||||
/// // Read the state back
|
||||
/// var readOps = new List<ActorStateReadOperation>
|
||||
/// {
|
||||
/// new GetValueOperation("name"),
|
||||
/// new ListKeysOperation(null), // List all keys
|
||||
/// new ListKeysOperation(null, "prefix_") // List keys starting with "prefix_"
|
||||
/// };
|
||||
/// var readResult = await storage.ReadStateAsync(actorId, readOps);
|
||||
/// </code>
|
||||
/// </example>
|
||||
public sealed class InMemoryActorStateStorage : IActorStateStorage
|
||||
{
|
||||
private static readonly ActivitySource ActivitySource = new("Microsoft.Extensions.AI.Agents.Runtime.Abstractions.InMemoryActorStateStorage");
|
||||
|
||||
private readonly ConcurrentDictionary<ActorId, ActorState> _actorStates = new();
|
||||
private readonly object _lockObject = new();
|
||||
private long _globalETagCounter = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the internal state of an actor including its key-value pairs and ETag.
|
||||
/// </summary>
|
||||
private sealed class ActorState
|
||||
{
|
||||
public ConcurrentDictionary<string, JsonElement> Data { get; } = new();
|
||||
public string ETag { get; set; } = "0";
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask<WriteResponse> WriteStateAsync(ActorId actorId, IReadOnlyCollection<ActorStateWriteOperation> operations, string etag, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var activity = ActivitySource.StartActivity("actor.state write");
|
||||
|
||||
if (operations is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(operations));
|
||||
}
|
||||
|
||||
if (etag is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(etag));
|
||||
}
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
// Set telemetry attributes
|
||||
SetActorAttributes(activity, actorId);
|
||||
SetStateAttributes(activity, "write", operations.Count, etag);
|
||||
|
||||
try
|
||||
{
|
||||
lock (this._lockObject)
|
||||
{
|
||||
var actorState = this._actorStates.GetOrAdd(actorId, _ => new ActorState());
|
||||
|
||||
// Check ETag for optimistic concurrency control
|
||||
if (actorState.ETag != etag)
|
||||
{
|
||||
activity?.SetTag("state.success", false);
|
||||
activity?.SetTag("error.type", "etag_mismatch");
|
||||
activity?.SetStatus(ActivityStatusCode.Error, "ETag mismatch - concurrent modification detected");
|
||||
|
||||
// Return failure with current ETag
|
||||
return new ValueTask<WriteResponse>(new WriteResponse(actorState.ETag, success: false));
|
||||
}
|
||||
|
||||
// Apply all operations
|
||||
var operationTypes = new List<string>();
|
||||
foreach (var operation in operations)
|
||||
{
|
||||
switch (operation)
|
||||
{
|
||||
case SetValueOperation setValue:
|
||||
actorState.Data[setValue.Key] = setValue.Value;
|
||||
operationTypes.Add("set");
|
||||
break;
|
||||
|
||||
case RemoveKeyOperation removeKey:
|
||||
actorState.Data.TryRemove(removeKey.Key, out _);
|
||||
operationTypes.Add("remove");
|
||||
break;
|
||||
|
||||
default:
|
||||
var errorMessage = $"Unsupported write operation type: {operation.GetType().Name}";
|
||||
var exception = new InvalidOperationException(errorMessage);
|
||||
SetErrorAttributes(activity, exception);
|
||||
throw exception;
|
||||
}
|
||||
}
|
||||
|
||||
// Update ETag
|
||||
var newETag = Interlocked.Increment(ref this._globalETagCounter).ToString();
|
||||
actorState.ETag = newETag;
|
||||
|
||||
// Set success attributes
|
||||
SetOperationStatus(activity, true);
|
||||
activity?.SetTag("state.success", true);
|
||||
activity?.SetTag("state.new_etag", newETag);
|
||||
activity?.SetTag("state.operations", string.Join(",", operationTypes));
|
||||
|
||||
return new ValueTask<WriteResponse>(new WriteResponse(newETag, success: true));
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
SetErrorAttributes(activity, ex);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask<ReadResponse> ReadStateAsync(ActorId actorId, IReadOnlyCollection<ActorStateReadOperation> operations, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var activity = ActivitySource.StartActivity("actor.state read");
|
||||
|
||||
if (operations is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(operations));
|
||||
}
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
// Set telemetry attributes
|
||||
SetActorAttributes(activity, actorId);
|
||||
SetStateAttributes(activity, "read", operations.Count);
|
||||
|
||||
try
|
||||
{
|
||||
var actorState = this._actorStates.GetOrAdd(actorId, _ => new ActorState());
|
||||
var results = new List<ActorReadResult>();
|
||||
var operationTypes = new List<string>();
|
||||
|
||||
foreach (var operation in operations)
|
||||
{
|
||||
switch (operation)
|
||||
{
|
||||
case GetValueOperation getValue:
|
||||
var hasValue = actorState.Data.TryGetValue(getValue.Key, out var value);
|
||||
results.Add(new GetValueResult(hasValue ? value : null));
|
||||
operationTypes.Add($"get:{getValue.Key}");
|
||||
break;
|
||||
|
||||
case ListKeysOperation listKeys:
|
||||
var keys = actorState.Data.Keys.ToList();
|
||||
|
||||
// Filter keys by prefix if provided
|
||||
if (!string.IsNullOrEmpty(listKeys.KeyPrefix))
|
||||
{
|
||||
keys = [.. keys.Where(key => key.StartsWith(listKeys.KeyPrefix, StringComparison.Ordinal))];
|
||||
}
|
||||
|
||||
// Handle pagination if continuation token is provided
|
||||
if (!string.IsNullOrEmpty(listKeys.ContinuationToken))
|
||||
{
|
||||
// For this simple implementation, we'll parse the continuation token as an index
|
||||
if (int.TryParse(listKeys.ContinuationToken, out int startIndex) && startIndex < keys.Count)
|
||||
{
|
||||
keys = [.. keys.Skip(startIndex)];
|
||||
}
|
||||
else
|
||||
{
|
||||
keys = [];
|
||||
}
|
||||
}
|
||||
|
||||
// For simplicity, we'll return all keys without pagination
|
||||
// In a real implementation, you might want to implement proper pagination
|
||||
results.Add(new ListKeysResult(keys.AsReadOnly(), continuationToken: null));
|
||||
operationTypes.Add($"list:{listKeys.KeyPrefix ?? "*"}");
|
||||
break;
|
||||
|
||||
default:
|
||||
var errorMessage = $"Unsupported read operation type: {operation.GetType().Name}";
|
||||
var exception = new InvalidOperationException(errorMessage);
|
||||
SetErrorAttributes(activity, exception);
|
||||
throw exception;
|
||||
}
|
||||
}
|
||||
|
||||
// Set success attributes
|
||||
SetOperationStatus(activity, true);
|
||||
activity?.SetTag("state.etag", actorState.ETag);
|
||||
activity?.SetTag("state.operations", string.Join(",", operationTypes));
|
||||
activity?.SetTag("state.success", true);
|
||||
|
||||
return new ValueTask<ReadResponse>(new ReadResponse(actorState.ETag, results.AsReadOnly()));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
SetErrorAttributes(activity, ex);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears all stored actor state. This method is primarily intended for testing scenarios.
|
||||
/// </summary>
|
||||
public void Clear()
|
||||
{
|
||||
lock (this._lockObject)
|
||||
{
|
||||
this._actorStates.Clear();
|
||||
Interlocked.Exchange(ref this._globalETagCounter, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current count of actors that have state stored.
|
||||
/// </summary>
|
||||
/// <returns>The number of actors with stored state.</returns>
|
||||
public int ActorCount => this._actorStates.Count;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current count of keys stored for a specific actor.
|
||||
/// </summary>
|
||||
/// <param name="actorId">The actor identifier.</param>
|
||||
/// <returns>The number of keys stored for the specified actor, or 0 if the actor has no state.</returns>
|
||||
public int GetKeyCount(ActorId actorId)
|
||||
{
|
||||
return this._actorStates.TryGetValue(actorId, out var state) ? state.Data.Count : 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current ETag for a specific actor.
|
||||
/// </summary>
|
||||
/// <param name="actorId">The actor identifier.</param>
|
||||
/// <returns>The current ETag for the specified actor, or "0" if the actor has no state.</returns>
|
||||
public string GetETag(ActorId actorId)
|
||||
{
|
||||
return this._actorStates.TryGetValue(actorId, out var state) ? state.ETag : "0";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets actor attributes on an activity.
|
||||
/// </summary>
|
||||
/// <param name="activity">The activity to set attributes on.</param>
|
||||
/// <param name="actorId">The actor ID.</param>
|
||||
private static void SetActorAttributes(Activity? activity, ActorId actorId)
|
||||
{
|
||||
if (activity == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
activity.SetTag("actor.id", actorId.ToString());
|
||||
activity.SetTag("actor.type", actorId.Type.Name);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets state operation attributes on an activity.
|
||||
/// </summary>
|
||||
/// <param name="activity">The activity to set attributes on.</param>
|
||||
/// <param name="operationType">The type of state operation.</param>
|
||||
/// <param name="operationCount">Optional count of operations.</param>
|
||||
/// <param name="etag">Optional ETag value.</param>
|
||||
private static void SetStateAttributes(Activity? activity, string operationType, int? operationCount = null, string? etag = null)
|
||||
{
|
||||
if (activity == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
activity.SetTag("state.operation.type", operationType);
|
||||
|
||||
if (operationCount.HasValue)
|
||||
{
|
||||
activity.SetTag("state.operation.count", operationCount.Value);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(etag))
|
||||
{
|
||||
activity.SetTag("state.etag", etag);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets success/failure status on an activity.
|
||||
/// </summary>
|
||||
/// <param name="activity">The activity to set status on.</param>
|
||||
/// <param name="success">Whether the operation was successful.</param>
|
||||
/// <param name="errorMessage">Optional error message for failures.</param>
|
||||
private static void SetOperationStatus(Activity? activity, bool success, string? errorMessage = null)
|
||||
{
|
||||
if (activity == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (success)
|
||||
{
|
||||
activity.SetStatus(ActivityStatusCode.Ok);
|
||||
}
|
||||
else
|
||||
{
|
||||
activity.SetStatus(ActivityStatusCode.Error, errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets error attributes on an activity.
|
||||
/// </summary>
|
||||
/// <param name="activity">The activity to set error attributes on.</param>
|
||||
/// <param name="exception">The exception that occurred.</param>
|
||||
private static void SetErrorAttributes(Activity? activity, Exception exception)
|
||||
{
|
||||
if (activity == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
activity.SetTag("error.type", exception.GetType().Name);
|
||||
activity.SetTag("error.message", exception.Message);
|
||||
activity.SetStatus(ActivityStatusCode.Error, exception.Message);
|
||||
|
||||
// Add exception event
|
||||
activity.AddEvent(new ActivityEvent("exception", DateTimeOffset.UtcNow, new ActivityTagsCollection
|
||||
{
|
||||
["error.type"] = exception.GetType().Name,
|
||||
["error.message"] = exception.Message,
|
||||
["error.stack_trace"] = exception.StackTrace
|
||||
}));
|
||||
}
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Text.Json.Serialization.Metadata;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// Provides extension methods for JSON serialization with source generation support.
|
||||
/// </summary>
|
||||
internal static class JsonSerializerExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the JsonTypeInfo for a type, preferring the one from options if available,
|
||||
/// otherwise falling back to the source-generated context.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type to get JsonTypeInfo for.</typeparam>
|
||||
/// <param name="options">The JsonSerializerOptions to check first.</param>
|
||||
/// <param name="fallbackContext">The fallback JsonSerializerContext to use if not found in options.</param>
|
||||
/// <returns>The JsonTypeInfo for the requested type.</returns>
|
||||
public static JsonTypeInfo<T> GetTypeInfo<T>(this JsonSerializerOptions options, JsonSerializerContext fallbackContext)
|
||||
{
|
||||
// Try to get from the options first (if a context is configured)
|
||||
if (options.TypeInfoResolver?.GetTypeInfo(typeof(T), options) is JsonTypeInfo<T> typeInfo)
|
||||
{
|
||||
return typeInfo;
|
||||
}
|
||||
|
||||
// Fall back to the provided source-generated context
|
||||
return (JsonTypeInfo<T>)fallbackContext.GetTypeInfo(typeof(T))!;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an operation to list keys from an actor's state, with optional pagination support.
|
||||
/// </summary>
|
||||
/// <param name="continuationToken">Optional token for pagination to continue listing from a previous operation.</param>
|
||||
/// <param name="keyPrefix">Optional prefix to filter keys. Only keys starting with this prefix will be returned.</param>
|
||||
public class ListKeysOperation(string? continuationToken, string? keyPrefix = null) : ActorStateReadOperation
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the continuation token for pagination.
|
||||
/// </summary>
|
||||
[JsonPropertyName("continuationToken")]
|
||||
public string? ContinuationToken { get; } = continuationToken;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the key prefix for filtering. Only keys starting with this prefix will be returned.
|
||||
/// </summary>
|
||||
[JsonPropertyName("keyPrefix")]
|
||||
public string? KeyPrefix { get; } = keyPrefix;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the type of the read operation.
|
||||
/// </summary>
|
||||
public override ActorReadOperationType Type => ActorReadOperationType.ListKeys;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the result of a list keys operation containing the found keys and optional continuation token.
|
||||
/// </summary>
|
||||
/// <param name="keys">The collection of keys found in the actor's state.</param>
|
||||
/// <param name="continuationToken">Optional token for pagination to retrieve additional keys.</param>
|
||||
public class ListKeysResult(IReadOnlyCollection<string> keys, string? continuationToken) : ActorReadResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the collection of keys found in the actor's state.
|
||||
/// </summary>
|
||||
[JsonPropertyName("keys")]
|
||||
public IReadOnlyCollection<string> Keys { get; } = keys;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the continuation token for pagination.
|
||||
/// </summary>
|
||||
[JsonPropertyName("continuationToken")]
|
||||
public string? ContinuationToken { get; } = continuationToken;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the type of the read result operation.
|
||||
/// </summary>
|
||||
public override ActorReadResultType Type => ActorReadResultType.ListKeys;
|
||||
}
|
||||
+6
@@ -5,12 +5,14 @@
|
||||
<TargetFrameworks Condition="'$(Configuration)' == 'Debug'">$(ProjectsDebugTargetFrameworks)</TargetFrameworks>
|
||||
<NoWarn>$(NoWarn);IDE1006;IDE0130</NoWarn>
|
||||
<VersionSuffix>alpha</VersionSuffix>
|
||||
<RootNamespace>Microsoft.Extensions.AI.Agents.Runtime</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<InjectSharedThrow>true</InjectSharedThrow>
|
||||
<InjectDiagnosticAttributesOnLegacy>true</InjectDiagnosticAttributesOnLegacy>
|
||||
<InjectTrimAttributesOnLegacy>true</InjectTrimAttributesOnLegacy>
|
||||
<InjectIsExternalInitOnLegacy>true</InjectIsExternalInitOnLegacy>
|
||||
</PropertyGroup>
|
||||
|
||||
<Import Project="$(RepoRoot)/dotnet/nuget/nuget-package.props" />
|
||||
@@ -23,6 +25,10 @@
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
|
||||
<PackageReference Include="System.Text.Json" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(TargetFrameworkIdentifier)' != '.NETCoreApp'">
|
||||
<PackageReference Include="Microsoft.Bcl.HashCode" />
|
||||
<PackageReference Include="System.Text.Json" />
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// The response of a read request for an actor.
|
||||
/// </summary>
|
||||
/// <param name="eTag">The actor's last-known ETag value.</param>
|
||||
/// <param name="results">The ordered collection of results.</param>
|
||||
public class ReadResponse(string eTag, IReadOnlyList<ActorReadResult> results)
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the version of the state update.
|
||||
/// </summary>
|
||||
[JsonPropertyName("etag")]
|
||||
public string ETag { get; } = eTag;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the ordered collection of read operation results.
|
||||
/// </summary>
|
||||
[JsonPropertyName("results")]
|
||||
public IReadOnlyList<ActorReadResult> Results { get; } = results;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an operation to remove a key from an actor's state.
|
||||
/// </summary>
|
||||
/// <param name="Key">The key to remove from the actor's state.</param>
|
||||
public sealed class RemoveKeyOperation(string Key) : ActorStateWriteOperation
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the key for the state operation.
|
||||
/// </summary>
|
||||
[JsonPropertyName("key")]
|
||||
public string Key { get; } = Key;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the type of the write operation.
|
||||
/// </summary>
|
||||
public override ActorWriteOperationType Type => ActorWriteOperationType.RemoveKey;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the status of a request in the actor system.
|
||||
/// </summary>
|
||||
public enum RequestStatus
|
||||
{
|
||||
/// <summary>
|
||||
/// The request is pending and has not yet been processed.
|
||||
/// </summary>
|
||||
[JsonStringEnumMemberName("pending")]
|
||||
Pending,
|
||||
|
||||
/// <summary>
|
||||
/// The request has been completed successfully.
|
||||
/// </summary>
|
||||
[JsonStringEnumMemberName("completed")]
|
||||
Completed,
|
||||
|
||||
/// <summary>
|
||||
/// The request has failed.
|
||||
/// </summary>
|
||||
[JsonStringEnumMemberName("failed")]
|
||||
Failed,
|
||||
|
||||
/// <summary>
|
||||
/// The request was not found, possibly due to it being deleted or never existing.
|
||||
/// </summary>
|
||||
[JsonStringEnumMemberName("not_found")]
|
||||
NotFound,
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an operation to send a request message to another actor.
|
||||
/// </summary>
|
||||
/// <param name="Message">The request message to send.</param>
|
||||
public sealed class SendRequestOperation(ActorRequestMessage Message) : ActorMessageWriteOperation
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the message to send.
|
||||
/// </summary>
|
||||
[JsonPropertyName("message")]
|
||||
public ActorRequestMessage Message { get; } = Message;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the type of the write operation.
|
||||
/// </summary>
|
||||
public override ActorWriteOperationType Type => ActorWriteOperationType.SendRequest;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an operation to set a key-value pair in an actor's state.
|
||||
/// </summary>
|
||||
/// <param name="Key">The key to set in the actor's state.</param>
|
||||
/// <param name="Value">The value to associate with the key.</param>
|
||||
public sealed class SetValueOperation(string Key, JsonElement Value) : ActorStateWriteOperation
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the key for the state operation.
|
||||
/// </summary>
|
||||
[JsonPropertyName("key")]
|
||||
public string Key { get; } = Key;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the value for the state operation.
|
||||
/// </summary>
|
||||
[JsonPropertyName("value")]
|
||||
public JsonElement Value { get; } = Value;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the type of the write operation.
|
||||
/// </summary>
|
||||
public override ActorWriteOperationType Type => ActorWriteOperationType.SetValue;
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an operation to update the status of an incoming request, possibly with a result.
|
||||
/// The MessageId must correspond to a non-terminated request in the actor's inbox (Status is Pending).
|
||||
/// </summary>
|
||||
/// <param name="MessageId">The identifier of the message to update.</param>
|
||||
/// <param name="Status">The new status for the request.</param>
|
||||
/// <param name="Data">The data associated with the status update (e.g., result for completed requests).</param>
|
||||
public sealed class UpdateRequestOperation(string MessageId, RequestStatus Status, JsonElement Data) : ActorMessageWriteOperation
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the identifier of the message to update.
|
||||
/// </summary>
|
||||
[JsonPropertyName("messageId")]
|
||||
public string MessageId { get; } = MessageId;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the new status for the request.
|
||||
/// </summary>
|
||||
[JsonPropertyName("status")]
|
||||
public RequestStatus Status { get; } = Status;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the data associated with the status update.
|
||||
/// </summary>
|
||||
[JsonPropertyName("data")]
|
||||
public JsonElement Data { get; } = Data;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the type of the write operation.
|
||||
/// </summary>
|
||||
public override ActorWriteOperationType Type => ActorWriteOperationType.UpdateRequest;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the response of a write request for an actor.
|
||||
/// </summary>
|
||||
/// <param name="eTag">The actor's updated ETag value after the write operation.</param>
|
||||
/// <param name="success">Whether the write operation was successful.</param>
|
||||
public class WriteResponse(string eTag, bool success)
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the version of the state update.
|
||||
/// </summary>
|
||||
[JsonPropertyName("etag")]
|
||||
public string ETag { get; } = eTag;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the write operation was successful.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If <c>false</c>, the write operation may have failed due to a concurrency conflict or other issue.
|
||||
/// In either case the <see cref="ETag"/> property will contain the last known ETag value of the actor's state.
|
||||
/// </remarks>
|
||||
[JsonPropertyName("success")]
|
||||
public bool Success { get; } = success;
|
||||
}
|
||||
@@ -0,0 +1,409 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using static Microsoft.Extensions.AI.Agents.Runtime.ActorRuntimeOpenTelemetryConsts;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// Helper methods for setting common telemetry attributes on activities.
|
||||
/// </summary>
|
||||
internal static class ActivityExtensions
|
||||
{
|
||||
public const string ActorCreated = ActorRuntimeOpenTelemetryConsts.EventInfo.Names.ActorCreated;
|
||||
public const string ActorStarted = ActorRuntimeOpenTelemetryConsts.EventInfo.Names.ActorStarted;
|
||||
public const string MessageSent = ActorRuntimeOpenTelemetryConsts.EventInfo.Names.MessageSent;
|
||||
public const string MessageReceived = ActorRuntimeOpenTelemetryConsts.EventInfo.Names.MessageReceived;
|
||||
public const string RequestCompleted = ActorRuntimeOpenTelemetryConsts.EventInfo.Names.RequestCompleted;
|
||||
|
||||
// Re-export common status values for convenience
|
||||
public const string Started = "started";
|
||||
public const string Sent = "sent";
|
||||
public const string Enqueued = "enqueued";
|
||||
public const string Created = "created";
|
||||
public const string Found = "found";
|
||||
public const string HandleCreated = "handle_created";
|
||||
|
||||
/// <summary>
|
||||
/// Sets common actor attributes on an activity.
|
||||
/// </summary>
|
||||
/// <param name="activity">The activity to set attributes on.</param>
|
||||
/// <param name="actorId">The actor ID.</param>
|
||||
/// <param name="operation">Optional operation name.</param>
|
||||
public static void SetActorAttributes(this System.Diagnostics.Activity? activity, ActorId actorId, string? operation = null)
|
||||
{
|
||||
if (activity == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
activity.SetTag(ActorRuntimeOpenTelemetryConsts.Actor.Id, actorId.ToString());
|
||||
activity.SetTag(ActorRuntimeOpenTelemetryConsts.Actor.Type, actorId.Type.Name);
|
||||
activity.SetTag(ActorRuntimeOpenTelemetryConsts.Actor.RpcSystem, ActorRuntimeOpenTelemetryConsts.Actor.SystemName);
|
||||
|
||||
if (!string.IsNullOrEmpty(operation))
|
||||
{
|
||||
activity.SetTag(ActorRuntimeOpenTelemetryConsts.Actor.Operation, operation);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets common message attributes on an activity.
|
||||
/// </summary>
|
||||
/// <param name="activity">The activity to set attributes on.</param>
|
||||
/// <param name="messageId">The message ID.</param>
|
||||
/// <param name="messageType">Optional message type.</param>
|
||||
/// <param name="method">Optional message method.</param>
|
||||
public static void SetMessageAttributes(this System.Diagnostics.Activity? activity, string messageId, string? messageType = null, string? method = null)
|
||||
{
|
||||
if (activity == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
activity.SetTag(Message.Id, messageId);
|
||||
|
||||
if (!string.IsNullOrEmpty(messageType))
|
||||
{
|
||||
activity.SetTag(Message.Type, messageType);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(method))
|
||||
{
|
||||
activity.SetTag(Message.Method, method);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets common request attributes on an activity.
|
||||
/// </summary>
|
||||
/// <param name="activity">The activity to set attributes on.</param>
|
||||
/// <param name="requestId">The request ID.</param>
|
||||
/// <param name="method">Optional request method.</param>
|
||||
/// <param name="timeout">Optional timeout value.</param>
|
||||
public static void SetRequestAttributes(this System.Diagnostics.Activity? activity, string requestId, string? method = null, System.TimeSpan? timeout = null)
|
||||
{
|
||||
if (activity == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
activity.SetTag(Request.Id, requestId);
|
||||
|
||||
if (!string.IsNullOrEmpty(method))
|
||||
{
|
||||
activity.SetTag(Request.Method, method);
|
||||
}
|
||||
|
||||
if (timeout.HasValue)
|
||||
{
|
||||
activity.SetTag(Request.Timeout, timeout.Value.TotalMilliseconds);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets common state operation attributes on an activity.
|
||||
/// </summary>
|
||||
/// <param name="activity">The activity to set attributes on.</param>
|
||||
/// <param name="operationType">The type of state operation.</param>
|
||||
/// <param name="operationCount">Optional count of operations.</param>
|
||||
/// <param name="etag">Optional ETag value.</param>
|
||||
public static void SetStateAttributes(this System.Diagnostics.Activity? activity, string operationType, int? operationCount = null, string? etag = null)
|
||||
{
|
||||
if (activity == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
activity.SetTag(State.OperationType, operationType);
|
||||
|
||||
if (operationCount.HasValue)
|
||||
{
|
||||
activity.SetTag(State.OperationCount, operationCount.Value);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(etag))
|
||||
{
|
||||
activity.SetTag(State.ETag, etag);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets success/failure status on an activity.
|
||||
/// </summary>
|
||||
/// <param name="activity">The activity to set status on.</param>
|
||||
/// <param name="success">Whether the operation was successful.</param>
|
||||
/// <param name="errorMessage">Optional error message for failures.</param>
|
||||
public static void SetOperationStatus(this System.Diagnostics.Activity? activity, bool success, string? errorMessage = null)
|
||||
{
|
||||
if (activity == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (success)
|
||||
{
|
||||
activity.SetStatus(System.Diagnostics.ActivityStatusCode.Ok);
|
||||
}
|
||||
else
|
||||
{
|
||||
activity.SetStatus(System.Diagnostics.ActivityStatusCode.Error, errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets error attributes on an activity.
|
||||
/// </summary>
|
||||
/// <param name="activity">The activity to set error attributes on.</param>
|
||||
/// <param name="exception">The exception that occurred.</param>
|
||||
/// <param name="errorType">Optional custom error type.</param>
|
||||
public static void SetErrorAttributes(this System.Diagnostics.Activity? activity, System.Exception exception, string? errorType = null)
|
||||
{
|
||||
if (activity == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
activity.SetTag(ErrorInfo.Type, errorType ?? exception.GetType().Name);
|
||||
activity.SetTag(ErrorInfo.Message, exception.Message);
|
||||
activity.SetStatus(System.Diagnostics.ActivityStatusCode.Error, exception.Message);
|
||||
|
||||
// Add exception event
|
||||
activity.AddEvent(new System.Diagnostics.ActivityEvent("exception", System.DateTimeOffset.UtcNow, new System.Diagnostics.ActivityTagsCollection
|
||||
{
|
||||
[ErrorInfo.Type] = errorType ?? exception.GetType().Name,
|
||||
[ErrorInfo.Message] = exception.Message,
|
||||
[ErrorInfo.StackTrace] = exception.StackTrace
|
||||
}));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets RPC-style attributes for actor operations.
|
||||
/// </summary>
|
||||
/// <param name="activity">The activity to set attributes on.</param>
|
||||
/// <param name="service">The RPC service name.</param>
|
||||
/// <param name="method">The RPC method name.</param>
|
||||
public static void SetRpcAttributes(this System.Diagnostics.Activity? activity, string service, string method)
|
||||
{
|
||||
if (activity == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
activity.SetTag(Actor.RpcSystem, Actor.SystemName);
|
||||
activity.SetTag(Actor.RpcService, service);
|
||||
activity.SetTag(Actor.RpcMethod, method);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets up complete telemetry for actor retrieval/creation operations.
|
||||
/// </summary>
|
||||
/// <param name="activity">The activity to set attributes on.</param>
|
||||
/// <param name="actorId">The actor ID.</param>
|
||||
/// <param name="exists">Whether the actor already exists.</param>
|
||||
/// <param name="started">Whether the actor was started.</param>
|
||||
public static void SetupActorOperation(this System.Diagnostics.Activity? activity, ActorId actorId, bool? exists = null, bool? started = null)
|
||||
{
|
||||
if (activity == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
SetActorAttributes(activity, actorId);
|
||||
SetRpcAttributes(activity, "ActorRuntime", "GetOrCreateActor");
|
||||
|
||||
if (exists.HasValue)
|
||||
{
|
||||
activity.SetTag(Actor.Exists, exists.Value);
|
||||
}
|
||||
|
||||
if (started.HasValue)
|
||||
{
|
||||
activity.SetTag(Actor.Started, started.Value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets up complete telemetry for message operations.
|
||||
/// </summary>
|
||||
/// <param name="activity">The activity to set attributes on.</param>
|
||||
/// <param name="actorId">The actor ID.</param>
|
||||
/// <param name="messageId">The message ID.</param>
|
||||
/// <param name="messageType">Optional message type.</param>
|
||||
/// <param name="method">Optional message method.</param>
|
||||
/// <param name="status">Optional message status.</param>
|
||||
public static void SetupMessageOperation(this System.Diagnostics.Activity? activity, ActorId actorId, string messageId, string? messageType = null, string? method = null, string? status = null)
|
||||
{
|
||||
if (activity == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
SetActorAttributes(activity, actorId);
|
||||
SetMessageAttributes(activity, messageId, messageType, method);
|
||||
|
||||
if (!string.IsNullOrEmpty(status))
|
||||
{
|
||||
activity.SetTag(Message.Status, status);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets up complete telemetry for request operations.
|
||||
/// </summary>
|
||||
/// <param name="activity">The activity to set attributes on.</param>
|
||||
/// <param name="actorId">The actor ID.</param>
|
||||
/// <param name="requestId">The request ID.</param>
|
||||
/// <param name="method">Optional request method.</param>
|
||||
/// <param name="service">The RPC service name.</param>
|
||||
/// <param name="rpcMethod">The RPC method name.</param>
|
||||
/// <param name="timeout">Optional timeout value.</param>
|
||||
public static void SetupRequestOperation(this System.Diagnostics.Activity? activity, ActorId actorId, string requestId, string? method = null, string service = "ActorClient", string rpcMethod = "SendRequest", System.TimeSpan? timeout = null)
|
||||
{
|
||||
if (activity == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
SetActorAttributes(activity, actorId);
|
||||
SetRequestAttributes(activity, requestId, method, timeout);
|
||||
SetRpcAttributes(activity, service, rpcMethod);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets up complete telemetry for state operations.
|
||||
/// </summary>
|
||||
/// <param name="activity">The activity to set attributes on.</param>
|
||||
/// <param name="actorId">The actor ID.</param>
|
||||
/// <param name="operationType">The type of state operation.</param>
|
||||
/// <param name="operationCount">Optional count of operations.</param>
|
||||
/// <param name="etag">Optional ETag value.</param>
|
||||
public static void SetupStateOperation(this System.Diagnostics.Activity? activity, ActorId actorId, string operationType, int? operationCount = null, string? etag = null)
|
||||
{
|
||||
if (activity == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
SetActorAttributes(activity, actorId);
|
||||
SetStateAttributes(activity, operationType, operationCount, etag);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records successful completion of an operation with optional additional attributes.
|
||||
/// </summary>
|
||||
/// <param name="activity">The activity to update.</param>
|
||||
/// <param name="additionalTags">Optional additional tags to set.</param>
|
||||
public static void RecordSuccess(this System.Diagnostics.Activity? activity, params (string key, object? value)[] additionalTags)
|
||||
{
|
||||
if (activity == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
SetOperationStatus(activity, true);
|
||||
|
||||
foreach (var (key, value) in additionalTags)
|
||||
{
|
||||
activity.SetTag(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records failure of an operation with error details.
|
||||
/// </summary>
|
||||
/// <param name="activity">The activity to update.</param>
|
||||
/// <param name="exception">The exception that occurred.</param>
|
||||
/// <param name="errorType">Optional custom error type.</param>
|
||||
/// <param name="additionalTags">Optional additional tags to set.</param>
|
||||
public static void RecordFailure(this System.Diagnostics.Activity? activity, System.Exception exception, string? errorType = null, params (string key, object? value)[] additionalTags)
|
||||
{
|
||||
if (activity == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
SetErrorAttributes(activity, exception, errorType);
|
||||
|
||||
foreach (var (key, value) in additionalTags)
|
||||
{
|
||||
activity.SetTag(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds an event with common actor context.
|
||||
/// </summary>
|
||||
/// <param name="activity">The activity to add the event to.</param>
|
||||
/// <param name="eventName">The name of the event.</param>
|
||||
/// <param name="actorId">The actor ID.</param>
|
||||
/// <param name="additionalData">Optional additional event data.</param>
|
||||
public static void AddActorEvent(this System.Diagnostics.Activity? activity, string eventName, ActorId actorId, params (string key, object? value)[] additionalData)
|
||||
{
|
||||
if (activity == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var tags = new System.Diagnostics.ActivityTagsCollection
|
||||
{
|
||||
[Actor.Id] = actorId.ToString(),
|
||||
[Actor.Type] = actorId.Type.Name
|
||||
};
|
||||
|
||||
foreach (var (key, value) in additionalData)
|
||||
{
|
||||
tags[key] = value;
|
||||
}
|
||||
|
||||
activity.AddEvent(new System.Diagnostics.ActivityEvent(eventName, System.DateTimeOffset.UtcNow, tags));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records successful completion and adds an event in a single terse call.
|
||||
/// </summary>
|
||||
/// <param name="activity">The activity to update.</param>
|
||||
/// <param name="eventName">The name of the event to add.</param>
|
||||
/// <param name="actorId">The actor ID for the event.</param>
|
||||
/// <param name="statusTags">Status tags to set on the activity.</param>
|
||||
/// <param name="eventData">Additional event data.</param>
|
||||
public static void CompleteWithEvent(this System.Diagnostics.Activity? activity, string eventName, ActorId actorId, (string key, object? value)[] statusTags, params (string key, object? value)[] eventData)
|
||||
{
|
||||
if (activity == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
RecordSuccess(activity, statusTags);
|
||||
AddActorEvent(activity, eventName, actorId, eventData);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Complete with event - ultra-terse single-line calls.
|
||||
/// </summary>
|
||||
public static void Complete(this System.Diagnostics.Activity? activity, string @event, ActorId actor, string status, params (string, object?)[] data) =>
|
||||
CompleteWithEvent(activity, @event, actor, [(Request.Status, status)], data);
|
||||
|
||||
/// <summary>
|
||||
/// Complete with multiple status tags and event.
|
||||
/// </summary>
|
||||
public static void Complete(this System.Diagnostics.Activity? activity, string @event, ActorId actor, (string, object?)[] status, params (string, object?)[] data) =>
|
||||
CompleteWithEvent(activity, @event, actor, status, data);
|
||||
|
||||
/// <summary>
|
||||
/// Record success with single status.
|
||||
/// </summary>
|
||||
public static void Success(this System.Diagnostics.Activity? activity, string status) =>
|
||||
RecordSuccess(activity, (Request.Status, status));
|
||||
|
||||
/// <summary>
|
||||
/// Add actor event.
|
||||
/// </summary>
|
||||
public static void Event(this System.Diagnostics.Activity? activity, string @event, ActorId actor, params (string, object?)[] data) =>
|
||||
AddActorEvent(activity, @event, actor, data);
|
||||
|
||||
/// <summary>
|
||||
/// Record failure.
|
||||
/// </summary>
|
||||
public static void Fail(this System.Diagnostics.Activity? activity, System.Exception exception, string? status = null) =>
|
||||
RecordFailure(activity, exception, null, status != null ? (Request.Status, status) : default);
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// Internal implementation of <see cref="IActorRuntimeBuilder"/> that manages actor type registrations
|
||||
/// and their associated factory methods for the actor runtime system.
|
||||
/// </summary>
|
||||
internal sealed class ActorRuntimeBuilder : IActorRuntimeBuilder
|
||||
{
|
||||
private readonly IHostApplicationBuilder _builder;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the collection of registered actor types and their corresponding factory methods.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// A dictionary where keys are <see cref="ActorType"/> instances and values are factory functions
|
||||
/// that create <see cref="IActor"/> instances given an <see cref="IServiceProvider"/> and <see cref="IActorRuntimeContext"/>.
|
||||
/// </value>
|
||||
public Dictionary<ActorType, Func<IServiceProvider, IActorRuntimeContext, IActor>> ActorFactories { get; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or creates an <see cref="ActorRuntimeBuilder"/> instance for the specified host application builder.
|
||||
/// If an instance already exists in the service collection, it returns the existing instance.
|
||||
/// Otherwise, it creates a new instance and registers it as a singleton service.
|
||||
/// </summary>
|
||||
/// <param name="builder">The host application builder to associate with the actor runtime builder.</param>
|
||||
/// <returns>
|
||||
/// An <see cref="ActorRuntimeBuilder"/> instance that can be used to configure actor types.
|
||||
/// </returns>
|
||||
/// <exception cref="ArgumentNullException">Thrown when <paramref name="builder"/> is null.</exception>
|
||||
public static ActorRuntimeBuilder GetOrAdd(IHostApplicationBuilder builder)
|
||||
{
|
||||
Microsoft.Shared.Diagnostics.Throw.IfNull(builder);
|
||||
var services = builder.Services;
|
||||
var descriptor = services.FirstOrDefault(s => s.ImplementationInstance is ActorRuntimeBuilder);
|
||||
if (descriptor?.ImplementationInstance is not ActorRuntimeBuilder instance)
|
||||
{
|
||||
instance = new ActorRuntimeBuilder(builder);
|
||||
services.Add(ServiceDescriptor.Singleton(instance));
|
||||
instance.ConfigureServices(services);
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ActorRuntimeBuilder"/> class.
|
||||
/// </summary>
|
||||
/// <param name="builder">The host application builder to associate with this actor runtime builder.</param>
|
||||
private ActorRuntimeBuilder(IHostApplicationBuilder builder)
|
||||
{
|
||||
this._builder = builder;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers an actor type with its factory method in the actor runtime.
|
||||
/// </summary>
|
||||
/// <param name="type">The actor type to register.</param>
|
||||
/// <param name="activator">
|
||||
/// The factory method that creates instances of the actor. This function receives an
|
||||
/// <see cref="IServiceProvider"/> for dependency injection and an <see cref="IActorRuntimeContext"/>
|
||||
/// for the actor's runtime context, and returns an <see cref="IActor"/> instance.
|
||||
/// </param>
|
||||
/// <exception cref="ArgumentException">
|
||||
/// Thrown when an actor type with the same name is already registered.
|
||||
/// </exception>
|
||||
/// <remarks>
|
||||
/// Each actor type can only be registered once. Attempting to register the same actor type
|
||||
/// multiple times will result in an exception being thrown by the underlying dictionary.
|
||||
/// </remarks>
|
||||
public void AddActorType(ActorType type, Func<IServiceProvider, IActorRuntimeContext, IActor> activator)
|
||||
{
|
||||
this.ActorFactories.Add(type, activator);
|
||||
}
|
||||
|
||||
private void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
services.AddSingleton<IActorRuntimeBuilder>(this);
|
||||
services.AddSingleton<IActorStateStorage, InMemoryActorStateStorage>();
|
||||
services.AddSingleton<IActorClient, InProcessActorClient>();
|
||||
services.AddSingleton<InProcessActorRuntime>(sp =>
|
||||
{
|
||||
var jsonSerializerOptions = sp.GetService<JsonSerializerOptions>() ?? new();
|
||||
var actorStateStorage = sp.GetRequiredService<IActorStateStorage>();
|
||||
return new InProcessActorRuntime(sp, this.ActorFactories, actorStateStorage, jsonSerializerOptions);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// Provides extension methods for configuring actor runtime services in a host application.
|
||||
/// </summary>
|
||||
public static class ActorRuntimeHostingExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds actor runtime services to the specified host application builder.
|
||||
/// </summary>
|
||||
/// <param name="builder">The <see cref="IHostApplicationBuilder"/> to configure.</param>
|
||||
/// <returns>An <see cref="IActorRuntimeBuilder"/> that can be used to further configure the actor runtime.</returns>
|
||||
/// <exception cref="ArgumentNullException">Thrown when <paramref name="builder"/> is null.</exception>
|
||||
public static IActorRuntimeBuilder AddActorRuntime(this IHostApplicationBuilder builder)
|
||||
{
|
||||
return ActorRuntimeBuilder.GetOrAdd(builder);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.Runtime;
|
||||
|
||||
[JsonSourceGenerationOptions(
|
||||
JsonSerializerDefaults.Web,
|
||||
UseStringEnumConverter = true,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
WriteIndented = false)]
|
||||
[JsonSerializable(typeof(string))]
|
||||
internal sealed partial class ActorRuntimeJsonContext : JsonSerializerContext
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,782 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// Provides constants used by actor runtime telemetry services following OpenTelemetry semantic conventions.
|
||||
/// Extends the base agent telemetry with runtime-specific attributes and operations.
|
||||
/// </summary>
|
||||
internal static class ActorRuntimeOpenTelemetryConsts
|
||||
{
|
||||
/// <summary>
|
||||
/// The default source name for actor runtime telemetry.
|
||||
/// </summary>
|
||||
public const string DefaultSourceName = "Microsoft.Extensions.AI.Agents.Runtime";
|
||||
|
||||
/// <summary>
|
||||
/// The default source name for in-process actor runtime telemetry.
|
||||
/// </summary>
|
||||
public const string InProcessSourceName = "Microsoft.Extensions.AI.Agents.Runtime.InProcess";
|
||||
|
||||
/// <summary>
|
||||
/// The unit for count measurements.
|
||||
/// </summary>
|
||||
public const string CountUnit = "count";
|
||||
|
||||
/// <summary>
|
||||
/// The unit for byte measurements.
|
||||
/// </summary>
|
||||
public const string ByteUnit = "byte";
|
||||
|
||||
/// <summary>
|
||||
/// Constants for runtime operation names following OpenTelemetry semantic conventions.
|
||||
/// These operations align with RPC and GenAI conventions where applicable.
|
||||
/// </summary>
|
||||
public static class Operations
|
||||
{
|
||||
/// <summary>
|
||||
/// Actor creation operation.
|
||||
/// </summary>
|
||||
public const string CreateActor = "create_actor";
|
||||
|
||||
/// <summary>
|
||||
/// Actor retrieval operation.
|
||||
/// </summary>
|
||||
public const string GetActor = "get_actor";
|
||||
|
||||
/// <summary>
|
||||
/// Actor invocation operation (aligns with GenAI agent invoke conventions).
|
||||
/// </summary>
|
||||
public const string InvokeActor = "invoke_actor";
|
||||
|
||||
/// <summary>
|
||||
/// Actor start operation.
|
||||
/// </summary>
|
||||
public const string StartActor = "start_actor";
|
||||
|
||||
/// <summary>
|
||||
/// Actor stop operation.
|
||||
/// </summary>
|
||||
public const string StopActor = "stop_actor";
|
||||
|
||||
/// <summary>
|
||||
/// Actor dispose operation.
|
||||
/// </summary>
|
||||
public const string DisposeActor = "dispose_actor";
|
||||
|
||||
/// <summary>
|
||||
/// Message send operation.
|
||||
/// </summary>
|
||||
public const string SendMessage = "send_message";
|
||||
|
||||
/// <summary>
|
||||
/// Message receive operation.
|
||||
/// </summary>
|
||||
public const string ReceiveMessage = "receive_message";
|
||||
|
||||
/// <summary>
|
||||
/// Message process operation.
|
||||
/// </summary>
|
||||
public const string ProcessMessage = "process_message";
|
||||
|
||||
/// <summary>
|
||||
/// Request send operation (follows RPC client pattern).
|
||||
/// </summary>
|
||||
public const string SendRequest = "send_request";
|
||||
|
||||
/// <summary>
|
||||
/// Request receive operation (follows RPC server pattern).
|
||||
/// </summary>
|
||||
public const string ReceiveRequest = "receive_request";
|
||||
|
||||
/// <summary>
|
||||
/// Request process operation.
|
||||
/// </summary>
|
||||
public const string ProcessRequest = "process_request";
|
||||
|
||||
/// <summary>
|
||||
/// Response send operation.
|
||||
/// </summary>
|
||||
public const string SendResponse = "send_response";
|
||||
|
||||
/// <summary>
|
||||
/// Response receive operation.
|
||||
/// </summary>
|
||||
public const string ReceiveResponse = "receive_response";
|
||||
|
||||
/// <summary>
|
||||
/// Progress update operation.
|
||||
/// </summary>
|
||||
public const string ProgressUpdate = "progress_update";
|
||||
|
||||
/// <summary>
|
||||
/// State read operation.
|
||||
/// </summary>
|
||||
public const string StateRead = "state_read";
|
||||
|
||||
/// <summary>
|
||||
/// State write operation.
|
||||
/// </summary>
|
||||
public const string StateWrite = "state_write";
|
||||
|
||||
/// <summary>
|
||||
/// Actor runtime initialization operation.
|
||||
/// </summary>
|
||||
public const string InitializeRuntime = "initialize_runtime";
|
||||
|
||||
/// <summary>
|
||||
/// Actor runtime shutdown operation.
|
||||
/// </summary>
|
||||
public const string ShutdownRuntime = "shutdown_runtime";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constants for span naming patterns following OpenTelemetry semantic conventions.
|
||||
/// Span names should be low-cardinality and follow the pattern: {namespace} {operation_name} [{target}]
|
||||
/// </summary>
|
||||
public static class SpanNames
|
||||
{
|
||||
/// <summary>
|
||||
/// Base pattern for actor operations: "actor {operation}"
|
||||
/// </summary>
|
||||
public const string ActorOperationPattern = "actor {0}";
|
||||
|
||||
/// <summary>
|
||||
/// Pattern for actor operations with specific actor type: "actor {operation} {actor_type}"
|
||||
/// </summary>
|
||||
public const string ActorOperationWithTypePattern = "actor {0} {1}";
|
||||
|
||||
/// <summary>
|
||||
/// Pattern for message operations: "actor.message {operation}"
|
||||
/// </summary>
|
||||
public const string MessageOperationPattern = "actor.message {0}";
|
||||
|
||||
/// <summary>
|
||||
/// Pattern for request operations: "actor.request {operation}"
|
||||
/// </summary>
|
||||
public const string RequestOperationPattern = "actor.request {0}";
|
||||
|
||||
/// <summary>
|
||||
/// Pattern for state operations: "actor.state {operation}"
|
||||
/// </summary>
|
||||
public const string StateOperationPattern = "actor.state {0}";
|
||||
|
||||
/// <summary>
|
||||
/// Pattern for runtime operations: "actor.runtime {operation}"
|
||||
/// </summary>
|
||||
public const string RuntimeOperationPattern = "actor.runtime {0}";
|
||||
|
||||
/// <summary>
|
||||
/// Formats a span name for actor operations.
|
||||
/// </summary>
|
||||
/// <param name="operation">The operation name</param>
|
||||
/// <returns>Formatted span name</returns>
|
||||
public static string FormatActorOperation(string operation) => $"actor {operation}";
|
||||
|
||||
/// <summary>
|
||||
/// Formats a span name for actor operations with actor type.
|
||||
/// </summary>
|
||||
/// <param name="operation">The operation name</param>
|
||||
/// <param name="actorType">The actor type</param>
|
||||
/// <returns>Formatted span name</returns>
|
||||
public static string FormatActorOperationWithType(string operation, string actorType) => $"actor {operation} {actorType}";
|
||||
|
||||
/// <summary>
|
||||
/// Formats a span name for message operations.
|
||||
/// </summary>
|
||||
/// <param name="operation">The operation name</param>
|
||||
/// <returns>Formatted span name</returns>
|
||||
public static string FormatMessageOperation(string operation) => $"actor.message {operation}";
|
||||
|
||||
/// <summary>
|
||||
/// Formats a span name for request operations.
|
||||
/// </summary>
|
||||
/// <param name="operation">The operation name</param>
|
||||
/// <returns>Formatted span name</returns>
|
||||
public static string FormatRequestOperation(string operation) => $"actor.request {operation}";
|
||||
|
||||
/// <summary>
|
||||
/// Formats a span name for state operations.
|
||||
/// </summary>
|
||||
/// <param name="operation">The operation name</param>
|
||||
/// <returns>Formatted span name</returns>
|
||||
public static string FormatStateOperation(string operation) => $"actor.state {operation}";
|
||||
|
||||
/// <summary>
|
||||
/// Formats a span name for runtime operations.
|
||||
/// </summary>
|
||||
/// <param name="operation">The operation name</param>
|
||||
/// <returns>Formatted span name</returns>
|
||||
public static string FormatRuntimeOperation(string operation) => $"actor.runtime {operation}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constants for actor-related telemetry attributes.
|
||||
/// </summary>
|
||||
public static class Actor
|
||||
{
|
||||
/// <summary>
|
||||
/// The attribute name for the actor ID.
|
||||
/// </summary>
|
||||
public const string Id = "actor.id";
|
||||
|
||||
/// <summary>
|
||||
/// The attribute name for the actor type.
|
||||
/// </summary>
|
||||
public const string Type = "actor.type";
|
||||
|
||||
/// <summary>
|
||||
/// The attribute name for the actor key.
|
||||
/// </summary>
|
||||
public const string Key = "actor.key";
|
||||
|
||||
/// <summary>
|
||||
/// The attribute name for the actor operation.
|
||||
/// </summary>
|
||||
public const string Operation = "actor.operation";
|
||||
|
||||
/// <summary>
|
||||
/// The attribute name for whether the actor exists.
|
||||
/// </summary>
|
||||
public const string Exists = "actor.exists";
|
||||
|
||||
/// <summary>
|
||||
/// The attribute name for whether the actor was started.
|
||||
/// </summary>
|
||||
public const string Started = "actor.started";
|
||||
|
||||
/// <summary>
|
||||
/// The attribute name for the actor runtime type.
|
||||
/// </summary>
|
||||
public const string RuntimeType = "actor.runtime.type";
|
||||
|
||||
/// <summary>
|
||||
/// The attribute name for the actor state.
|
||||
/// </summary>
|
||||
public const string State = "actor.state";
|
||||
|
||||
/// <summary>
|
||||
/// RPC system identifier for actor runtime (follows RPC semantic conventions).
|
||||
/// </summary>
|
||||
public const string RpcSystem = "rpc.system";
|
||||
|
||||
/// <summary>
|
||||
/// RPC service name for actor runtime (follows RPC semantic conventions).
|
||||
/// </summary>
|
||||
public const string RpcService = "rpc.service";
|
||||
|
||||
/// <summary>
|
||||
/// RPC method name for actor runtime (follows RPC semantic conventions).
|
||||
/// </summary>
|
||||
public const string RpcMethod = "rpc.method";
|
||||
|
||||
/// <summary>
|
||||
/// The system name for actor runtime operations.
|
||||
/// </summary>
|
||||
public const string SystemName = "actor_runtime";
|
||||
|
||||
/// <summary>
|
||||
/// Constants for actor lifecycle attributes.
|
||||
/// </summary>
|
||||
public static class Lifecycle
|
||||
{
|
||||
/// <summary>
|
||||
/// The attribute name for the actor creation time.
|
||||
/// </summary>
|
||||
public const string CreatedAt = "actor.lifecycle.created_at";
|
||||
|
||||
/// <summary>
|
||||
/// The attribute name for the actor start time.
|
||||
/// </summary>
|
||||
public const string StartedAt = "actor.lifecycle.started_at";
|
||||
|
||||
/// <summary>
|
||||
/// The attribute name for the actor stop time.
|
||||
/// </summary>
|
||||
public const string StoppedAt = "actor.lifecycle.stopped_at";
|
||||
|
||||
/// <summary>
|
||||
/// The attribute name for the actor uptime.
|
||||
/// </summary>
|
||||
public const string Uptime = "actor.lifecycle.uptime";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constants for actor context attributes.
|
||||
/// </summary>
|
||||
public static class Context
|
||||
{
|
||||
/// <summary>
|
||||
/// The attribute name for the actor context type.
|
||||
/// </summary>
|
||||
public const string Type = "actor.context.type";
|
||||
|
||||
/// <summary>
|
||||
/// The attribute name for the actor context status.
|
||||
/// </summary>
|
||||
public const string Status = "actor.context.status";
|
||||
|
||||
/// <summary>
|
||||
/// The attribute name for the actor context error.
|
||||
/// </summary>
|
||||
public const string Error = "actor.context.error";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constants for actor performance metrics.
|
||||
/// </summary>
|
||||
public static class Performance
|
||||
{
|
||||
/// <summary>
|
||||
/// The attribute name for messages processed count.
|
||||
/// </summary>
|
||||
public const string MessagesProcessed = "actor.performance.messages_processed";
|
||||
|
||||
/// <summary>
|
||||
/// The attribute name for requests processed count.
|
||||
/// </summary>
|
||||
public const string RequestsProcessed = "actor.performance.requests_processed";
|
||||
|
||||
/// <summary>
|
||||
/// The attribute name for processing time.
|
||||
/// </summary>
|
||||
public const string ProcessingTime = "actor.performance.processing_time";
|
||||
|
||||
/// <summary>
|
||||
/// The attribute name for queue size.
|
||||
/// </summary>
|
||||
public const string QueueSize = "actor.performance.queue_size";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constants for message-related telemetry attributes.
|
||||
/// </summary>
|
||||
public static class Message
|
||||
{
|
||||
/// <summary>
|
||||
/// The attribute name for the message ID.
|
||||
/// </summary>
|
||||
public const string Id = "message.id";
|
||||
|
||||
/// <summary>
|
||||
/// The attribute name for the message type.
|
||||
/// </summary>
|
||||
public const string Type = "message.type";
|
||||
|
||||
/// <summary>
|
||||
/// The attribute name for the message method.
|
||||
/// </summary>
|
||||
public const string Method = "message.method";
|
||||
|
||||
/// <summary>
|
||||
/// The attribute name for the message size in bytes.
|
||||
/// </summary>
|
||||
public const string Size = "message.size";
|
||||
|
||||
/// <summary>
|
||||
/// The attribute name for the message timestamp.
|
||||
/// </summary>
|
||||
public const string Timestamp = "message.timestamp";
|
||||
|
||||
/// <summary>
|
||||
/// The attribute name for the message sender.
|
||||
/// </summary>
|
||||
public const string Sender = "message.sender";
|
||||
|
||||
/// <summary>
|
||||
/// The attribute name for the message recipient.
|
||||
/// </summary>
|
||||
public const string Recipient = "message.recipient";
|
||||
|
||||
/// <summary>
|
||||
/// The attribute name for the message status.
|
||||
/// </summary>
|
||||
public const string Status = "message.status";
|
||||
|
||||
/// <summary>
|
||||
/// The attribute name for the message sequence number.
|
||||
/// </summary>
|
||||
public const string SequenceNumber = "message.sequence_number";
|
||||
|
||||
/// <summary>
|
||||
/// Constants for message processing attributes.
|
||||
/// </summary>
|
||||
public static class Processing
|
||||
{
|
||||
/// <summary>
|
||||
/// The attribute name for processing start time.
|
||||
/// </summary>
|
||||
public const string StartTime = "message.processing.start_time";
|
||||
|
||||
/// <summary>
|
||||
/// The attribute name for processing end time.
|
||||
/// </summary>
|
||||
public const string EndTime = "message.processing.end_time";
|
||||
|
||||
/// <summary>
|
||||
/// The attribute name for processing duration.
|
||||
/// </summary>
|
||||
public const string Duration = "message.processing.duration";
|
||||
|
||||
/// <summary>
|
||||
/// The attribute name for processing status.
|
||||
/// </summary>
|
||||
public const string Status = "message.processing.status";
|
||||
|
||||
/// <summary>
|
||||
/// The attribute name for processing error.
|
||||
/// </summary>
|
||||
public const string Error = "message.processing.error";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constants for request-related telemetry attributes.
|
||||
/// </summary>
|
||||
public static class Request
|
||||
{
|
||||
/// <summary>
|
||||
/// The attribute name for the request ID.
|
||||
/// </summary>
|
||||
public const string Id = "request.id";
|
||||
|
||||
/// <summary>
|
||||
/// The attribute name for the request method.
|
||||
/// </summary>
|
||||
public const string Method = "request.method";
|
||||
|
||||
/// <summary>
|
||||
/// The attribute name for the request status.
|
||||
/// </summary>
|
||||
public const string Status = "request.status";
|
||||
|
||||
/// <summary>
|
||||
/// The attribute name for the request timeout.
|
||||
/// </summary>
|
||||
public const string Timeout = "request.timeout";
|
||||
|
||||
/// <summary>
|
||||
/// The attribute name for whether the request was cancelled.
|
||||
/// </summary>
|
||||
public const string Cancelled = "request.cancelled";
|
||||
|
||||
/// <summary>
|
||||
/// The attribute name for the request retry count.
|
||||
/// </summary>
|
||||
public const string RetryCount = "request.retry_count";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constants for response-related telemetry attributes.
|
||||
/// </summary>
|
||||
public static class Response
|
||||
{
|
||||
/// <summary>
|
||||
/// The attribute name for the response ID.
|
||||
/// </summary>
|
||||
public const string Id = "response.id";
|
||||
|
||||
/// <summary>
|
||||
/// The attribute name for the response status.
|
||||
/// </summary>
|
||||
public const string Status = "response.status";
|
||||
|
||||
/// <summary>
|
||||
/// The attribute name for the response size.
|
||||
/// </summary>
|
||||
public const string Size = "response.size";
|
||||
|
||||
/// <summary>
|
||||
/// The attribute name for the response type.
|
||||
/// </summary>
|
||||
public const string Type = "response.type";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constants for state-related telemetry attributes.
|
||||
/// </summary>
|
||||
public static class State
|
||||
{
|
||||
/// <summary>
|
||||
/// The attribute name for the state operation type.
|
||||
/// </summary>
|
||||
public const string OperationType = "state.operation.type";
|
||||
|
||||
/// <summary>
|
||||
/// The attribute name for the state operation count.
|
||||
/// </summary>
|
||||
public const string OperationCount = "state.operation.count";
|
||||
|
||||
/// <summary>
|
||||
/// The attribute name for the state result count.
|
||||
/// </summary>
|
||||
public const string ResultCount = "state.result.count";
|
||||
|
||||
/// <summary>
|
||||
/// The attribute name for the state operation success.
|
||||
/// </summary>
|
||||
public const string Success = "state.success";
|
||||
|
||||
/// <summary>
|
||||
/// The attribute name for the state ETag.
|
||||
/// </summary>
|
||||
public const string ETag = "state.etag";
|
||||
|
||||
/// <summary>
|
||||
/// The attribute name for the state size.
|
||||
/// </summary>
|
||||
public const string Size = "state.size";
|
||||
|
||||
/// <summary>
|
||||
/// The attribute name for the state key.
|
||||
/// </summary>
|
||||
public const string Key = "state.key";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constants for runtime client metrics.
|
||||
/// </summary>
|
||||
public static class Client
|
||||
{
|
||||
/// <summary>
|
||||
/// Constants for operation duration metrics.
|
||||
/// </summary>
|
||||
public static class OperationDuration
|
||||
{
|
||||
/// <summary>
|
||||
/// The description for the operation duration metric.
|
||||
/// </summary>
|
||||
public const string Description = "Measures the duration of actor runtime operations";
|
||||
|
||||
/// <summary>
|
||||
/// The name for the operation duration metric.
|
||||
/// </summary>
|
||||
public const string Name = "actor.runtime.client.operation.duration";
|
||||
|
||||
/// <summary>
|
||||
/// The explicit bucket boundaries for the operation duration histogram.
|
||||
/// </summary>
|
||||
public static readonly double[] ExplicitBucketBoundaries = [0.001, 0.005, 0.01, 0.02, 0.05, 0.1, 0.2, 0.5, 1.0, 2.0, 5.0, 10.0, 20.0, 50.0];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constants for message count metrics.
|
||||
/// </summary>
|
||||
public static class MessageCount
|
||||
{
|
||||
/// <summary>
|
||||
/// The description for the message count metric.
|
||||
/// </summary>
|
||||
public const string Description = "Measures the number of messages processed by actors";
|
||||
|
||||
/// <summary>
|
||||
/// The name for the message count metric.
|
||||
/// </summary>
|
||||
public const string Name = "actor.runtime.client.message.count";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constants for request count metrics.
|
||||
/// </summary>
|
||||
public static class RequestCount
|
||||
{
|
||||
/// <summary>
|
||||
/// The description for the request count metric.
|
||||
/// </summary>
|
||||
public const string Description = "Measures the number of requests processed by actors";
|
||||
|
||||
/// <summary>
|
||||
/// The name for the request count metric.
|
||||
/// </summary>
|
||||
public const string Name = "actor.runtime.client.request.count";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constants for actor count metrics.
|
||||
/// </summary>
|
||||
public static class ActorCount
|
||||
{
|
||||
/// <summary>
|
||||
/// The description for the actor count metric.
|
||||
/// </summary>
|
||||
public const string Description = "Measures the number of active actors";
|
||||
|
||||
/// <summary>
|
||||
/// The name for the actor count metric.
|
||||
/// </summary>
|
||||
public const string Name = "actor.runtime.client.actor.count";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constants for queue size metrics.
|
||||
/// </summary>
|
||||
public static class QueueSize
|
||||
{
|
||||
/// <summary>
|
||||
/// The description for the queue size metric.
|
||||
/// </summary>
|
||||
public const string Description = "Measures the size of actor message queues";
|
||||
|
||||
/// <summary>
|
||||
/// The name for the queue size metric.
|
||||
/// </summary>
|
||||
public const string Name = "actor.runtime.client.queue.size";
|
||||
|
||||
/// <summary>
|
||||
/// The explicit bucket boundaries for the queue size histogram.
|
||||
/// </summary>
|
||||
public static readonly int[] ExplicitBucketBoundaries = [0, 1, 5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constants for state operation metrics.
|
||||
/// </summary>
|
||||
public static class StateOperations
|
||||
{
|
||||
/// <summary>
|
||||
/// The description for the state operations metric.
|
||||
/// </summary>
|
||||
public const string Description = "Measures the number of state operations";
|
||||
|
||||
/// <summary>
|
||||
/// The name for the state operations metric.
|
||||
/// </summary>
|
||||
public const string Name = "actor.runtime.client.state.operations";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constants for error attributes.
|
||||
/// </summary>
|
||||
public static class ErrorInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// The attribute name for the error type (follows OpenTelemetry error conventions).
|
||||
/// </summary>
|
||||
public const string Type = "error.type";
|
||||
|
||||
/// <summary>
|
||||
/// The attribute name for the error message.
|
||||
/// </summary>
|
||||
public const string Message = "error.message";
|
||||
|
||||
/// <summary>
|
||||
/// The attribute name for the error stack trace.
|
||||
/// </summary>
|
||||
public const string StackTrace = "error.stack_trace";
|
||||
|
||||
/// <summary>
|
||||
/// Well-known error type for unknown errors.
|
||||
/// </summary>
|
||||
public const string TypeOther = "_OTHER";
|
||||
|
||||
/// <summary>
|
||||
/// Well-known error types for actor runtime operations.
|
||||
/// </summary>
|
||||
public static class Types
|
||||
{
|
||||
/// <summary>
|
||||
/// Actor not found error.
|
||||
/// </summary>
|
||||
public const string ActorNotFound = "actor_not_found";
|
||||
|
||||
/// <summary>
|
||||
/// Actor already exists error.
|
||||
/// </summary>
|
||||
public const string ActorAlreadyExists = "actor_already_exists";
|
||||
|
||||
/// <summary>
|
||||
/// Message delivery failure.
|
||||
/// </summary>
|
||||
public const string MessageDeliveryFailure = "message_delivery_failure";
|
||||
|
||||
/// <summary>
|
||||
/// Request timeout error.
|
||||
/// </summary>
|
||||
public const string RequestTimeout = "request_timeout";
|
||||
|
||||
/// <summary>
|
||||
/// State operation failure.
|
||||
/// </summary>
|
||||
public const string StateOperationFailure = "state_operation_failure";
|
||||
|
||||
/// <summary>
|
||||
/// Runtime initialization failure.
|
||||
/// </summary>
|
||||
public const string RuntimeInitializationFailure = "runtime_initialization_failure";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constants for event attributes and well-known event names.
|
||||
/// </summary>
|
||||
public static class EventInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// The attribute name for the event name.
|
||||
/// </summary>
|
||||
public const string Name = "event.name";
|
||||
|
||||
/// <summary>
|
||||
/// The attribute name for the event data.
|
||||
/// </summary>
|
||||
public const string Data = "event.data";
|
||||
|
||||
/// <summary>
|
||||
/// The attribute name for the event timestamp.
|
||||
/// </summary>
|
||||
public const string Timestamp = "event.timestamp";
|
||||
|
||||
/// <summary>
|
||||
/// Well-known event names for actor runtime operations.
|
||||
/// </summary>
|
||||
public static class Names
|
||||
{
|
||||
/// <summary>
|
||||
/// Actor created event.
|
||||
/// </summary>
|
||||
public const string ActorCreated = "actor.created";
|
||||
|
||||
/// <summary>
|
||||
/// Actor started event.
|
||||
/// </summary>
|
||||
public const string ActorStarted = "actor.started";
|
||||
|
||||
/// <summary>
|
||||
/// Actor stopped event.
|
||||
/// </summary>
|
||||
public const string ActorStopped = "actor.stopped";
|
||||
|
||||
/// <summary>
|
||||
/// Message sent event.
|
||||
/// </summary>
|
||||
public const string MessageSent = "actor.message.sent";
|
||||
|
||||
/// <summary>
|
||||
/// Message received event.
|
||||
/// </summary>
|
||||
public const string MessageReceived = "actor.message.received";
|
||||
|
||||
/// <summary>
|
||||
/// Request completed event.
|
||||
/// </summary>
|
||||
public const string RequestCompleted = "actor.request.completed";
|
||||
|
||||
/// <summary>
|
||||
/// State updated event.
|
||||
/// </summary>
|
||||
public const string StateUpdated = "actor.state.updated";
|
||||
|
||||
/// <summary>
|
||||
/// Runtime initialized event.
|
||||
/// </summary>
|
||||
public const string RuntimeInitialized = "actor.runtime.initialized";
|
||||
|
||||
/// <summary>
|
||||
/// Runtime shutdown event.
|
||||
/// </summary>
|
||||
public const string RuntimeShutdown = "actor.runtime.shutdown";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// High-performance logging messages using LoggerMessage source generator for InProcessActorContext.
|
||||
/// </summary>
|
||||
internal static partial class Log
|
||||
{
|
||||
// Actor context lifecycle logging
|
||||
[LoggerMessage(
|
||||
Level = LogLevel.Information,
|
||||
Message = "Actor context created: ActorId={ActorId}")]
|
||||
public static partial void ActorContextCreated(ILogger logger, string actorId);
|
||||
|
||||
[LoggerMessage(
|
||||
Level = LogLevel.Information,
|
||||
Message = "Actor context starting: ActorId={ActorId}")]
|
||||
public static partial void ActorContextStarting(ILogger logger, string actorId);
|
||||
|
||||
[LoggerMessage(
|
||||
Level = LogLevel.Information,
|
||||
Message = "Actor context started: ActorId={ActorId}")]
|
||||
public static partial void ActorContextStarted(ILogger logger, string actorId);
|
||||
|
||||
[LoggerMessage(
|
||||
Level = LogLevel.Information,
|
||||
Message = "Actor context disposing: ActorId={ActorId}")]
|
||||
public static partial void ActorContextDisposing(ILogger logger, string actorId);
|
||||
|
||||
[LoggerMessage(
|
||||
Level = LogLevel.Information,
|
||||
Message = "Actor context disposed: ActorId={ActorId}")]
|
||||
public static partial void ActorContextDisposed(ILogger logger, string actorId);
|
||||
|
||||
// Message handling logging
|
||||
[LoggerMessage(
|
||||
Level = LogLevel.Debug,
|
||||
Message = "Message enqueued: ActorId={ActorId}, MessageId={MessageId}, Type={MessageType}")]
|
||||
public static partial void MessageEnqueued(ILogger logger, string actorId, string messageId, string messageType);
|
||||
|
||||
[LoggerMessage(
|
||||
Level = LogLevel.Debug,
|
||||
Message = "Message yielded: ActorId={ActorId}, MessageId={MessageId}, Type={MessageType}, Count={MessageCount}")]
|
||||
public static partial void MessageYielded(ILogger logger, string actorId, string messageId, string messageType, int messageCount);
|
||||
|
||||
[LoggerMessage(
|
||||
Level = LogLevel.Debug,
|
||||
Message = "Watch messages started: ActorId={ActorId}")]
|
||||
public static partial void WatchMessagesStarted(ILogger logger, string actorId);
|
||||
|
||||
[LoggerMessage(
|
||||
Level = LogLevel.Debug,
|
||||
Message = "Watch messages completed: ActorId={ActorId}, TotalMessages={MessageCount}")]
|
||||
public static partial void WatchMessagesCompleted(ILogger logger, string actorId, int messageCount);
|
||||
|
||||
// Request handling logging
|
||||
[LoggerMessage(
|
||||
Level = LogLevel.Debug,
|
||||
Message = "Send request started: ActorId={ActorId}, MessageId={MessageId}")]
|
||||
public static partial void SendRequestStarted(ILogger logger, string actorId, string messageId);
|
||||
|
||||
[LoggerMessage(
|
||||
Level = LogLevel.Debug,
|
||||
Message = "Request message created: ActorId={ActorId}, MessageId={MessageId}, Method={Method}")]
|
||||
public static partial void RequestMessageCreated(ILogger logger, string actorId, string messageId, string method);
|
||||
|
||||
[LoggerMessage(
|
||||
Level = LogLevel.Debug,
|
||||
Message = "Request message found in inbox: ActorId={ActorId}, MessageId={MessageId}")]
|
||||
public static partial void RequestMessageFound(ILogger logger, string actorId, string messageId);
|
||||
|
||||
[LoggerMessage(
|
||||
Level = LogLevel.Debug,
|
||||
Message = "Response handle created: ActorId={ActorId}, MessageId={MessageId}")]
|
||||
public static partial void ResponseHandleCreated(ILogger logger, string actorId, string messageId);
|
||||
|
||||
// Progress update logging
|
||||
[LoggerMessage(
|
||||
Level = LogLevel.Debug,
|
||||
Message = "Progress update received: ActorId={ActorId}, MessageId={MessageId}, SequenceNumber={SequenceNumber}")]
|
||||
public static partial void ProgressUpdateReceived(ILogger logger, string actorId, string messageId, int sequenceNumber);
|
||||
|
||||
[LoggerMessage(
|
||||
Level = LogLevel.Debug,
|
||||
Message = "Progress update published: ActorId={ActorId}, MessageId={MessageId}")]
|
||||
public static partial void ProgressUpdatePublished(ILogger logger, string actorId, string messageId);
|
||||
|
||||
[LoggerMessage(
|
||||
Level = LogLevel.Error,
|
||||
Message = "Progress update failed: ActorId={ActorId}, MessageId={MessageId}, Reason={Reason}")]
|
||||
public static partial void ProgressUpdateFailed(ILogger logger, string actorId, string messageId, string reason);
|
||||
|
||||
// Storage operation logging
|
||||
[LoggerMessage(
|
||||
Level = LogLevel.Debug,
|
||||
Message = "Read operation started: ActorId={ActorId}, OperationCount={OperationCount}")]
|
||||
public static partial void ReadOperationStarted(ILogger logger, string actorId, int operationCount);
|
||||
|
||||
[LoggerMessage(
|
||||
Level = LogLevel.Debug,
|
||||
Message = "Read operation completed: ActorId={ActorId}, ResultCount={ResultCount}")]
|
||||
public static partial void ReadOperationCompleted(ILogger logger, string actorId, int resultCount);
|
||||
|
||||
[LoggerMessage(
|
||||
Level = LogLevel.Debug,
|
||||
Message = "Write operation started: ActorId={ActorId}, OperationCount={OperationCount}")]
|
||||
public static partial void WriteOperationStarted(ILogger logger, string actorId, int operationCount);
|
||||
|
||||
[LoggerMessage(
|
||||
Level = LogLevel.Debug,
|
||||
Message = "Write operation completed: ActorId={ActorId}, Success={Success}")]
|
||||
public static partial void WriteOperationCompleted(ILogger logger, string actorId, bool success);
|
||||
|
||||
[LoggerMessage(
|
||||
Level = LogLevel.Debug,
|
||||
Message = "Send request operation encountered: ActorId={ActorId}")]
|
||||
public static partial void SendRequestOperationEncountered(ILogger logger, string actorId);
|
||||
|
||||
[LoggerMessage(
|
||||
Level = LogLevel.Debug,
|
||||
Message = "Update request operation processing: ActorId={ActorId}, MessageId={MessageId}")]
|
||||
public static partial void UpdateRequestOperationProcessing(ILogger logger, string actorId, string messageId);
|
||||
|
||||
[LoggerMessage(
|
||||
Level = LogLevel.Debug,
|
||||
Message = "Operation processing completed: ActorId={ActorId}, ProcessedCount={ProcessedCount}")]
|
||||
public static partial void OperationProcessingCompleted(ILogger logger, string actorId, int processedCount);
|
||||
}
|
||||
@@ -0,0 +1,426 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Channels;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using static Microsoft.Extensions.AI.Agents.Runtime.ActivityExtensions;
|
||||
using Tel = Microsoft.Extensions.AI.Agents.Runtime.ActorRuntimeOpenTelemetryConsts;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.Runtime;
|
||||
|
||||
internal sealed class InProcessActorContext : IActorRuntimeContext, IAsyncDisposable, IDisposable
|
||||
{
|
||||
private static readonly ActivitySource ActivitySource = new(ActorRuntimeOpenTelemetryConsts.InProcessSourceName);
|
||||
|
||||
private readonly CancellationTokenSource _cts = new();
|
||||
private readonly Channel<ActorMessage> _pendingMessages = Channel.CreateUnbounded<ActorMessage>();
|
||||
private readonly object _lock = new();
|
||||
private readonly Dictionary<string, ActorInboxEntry> _inbox = [];
|
||||
private readonly InProcessActorRuntime _runtime;
|
||||
private readonly IActor _actorInstance;
|
||||
private readonly ILogger<InProcessActorContext> _logger;
|
||||
private Task? _actorRunTask;
|
||||
|
||||
public InProcessActorContext(
|
||||
ActorId ActorId,
|
||||
InProcessActorRuntime runtime,
|
||||
Func<IServiceProvider, IActorRuntimeContext, IActor> actorFactory)
|
||||
{
|
||||
this._runtime = runtime;
|
||||
this.ActorId = ActorId;
|
||||
this._logger = runtime.Services.GetRequiredService<ILogger<InProcessActorContext>>();
|
||||
this._actorInstance = actorFactory(runtime.Services, this);
|
||||
|
||||
Log.ActorContextCreated(this._logger, this.ActorId.ToString());
|
||||
}
|
||||
|
||||
public ActorId ActorId { get; }
|
||||
|
||||
private IActorStateStorage Storage => this._runtime.Storage;
|
||||
|
||||
public void Start()
|
||||
{
|
||||
using var activity = ActivitySource.StartActivity(
|
||||
ActorRuntimeOpenTelemetryConsts.SpanNames.FormatActorOperation(ActorRuntimeOpenTelemetryConsts.Operations.StartActor));
|
||||
|
||||
activity.SetActorAttributes(this.ActorId, "start");
|
||||
|
||||
try
|
||||
{
|
||||
Log.ActorContextStarting(this._logger, this.ActorId.ToString());
|
||||
this._actorRunTask = this._actorInstance.RunAsync(this._cts.Token).AsTask();
|
||||
Log.ActorContextStarted(this._logger, this.ActorId.ToString());
|
||||
|
||||
activity.Complete(ActorStarted, this.ActorId, [(Tel.Actor.Started, true)]);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
activity.Fail(ex);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public void EnqueueMessage(ActorMessage message)
|
||||
{
|
||||
using var activity = ActivitySource.StartActivity(
|
||||
ActorRuntimeOpenTelemetryConsts.SpanNames.FormatMessageOperation(ActorRuntimeOpenTelemetryConsts.Operations.ReceiveMessage));
|
||||
|
||||
var messageId = message switch
|
||||
{
|
||||
ActorRequestMessage requestMessage => requestMessage.MessageId,
|
||||
ActorResponseMessage responseMessage => responseMessage.MessageId,
|
||||
_ => "unknown"
|
||||
};
|
||||
|
||||
// Set message tracing attributes
|
||||
activity.SetActorAttributes(this.ActorId);
|
||||
activity.SetMessageAttributes(messageId, message.Type.ToString());
|
||||
|
||||
try
|
||||
{
|
||||
Log.MessageEnqueued(this._logger, this.ActorId.ToString(), messageId, message.Type.ToString());
|
||||
this._pendingMessages.Writer.TryWrite(message);
|
||||
|
||||
activity.Complete(MessageReceived, this.ActorId, Enqueued,
|
||||
(Tel.Message.Id, messageId), (Tel.Message.Type, message.Type.ToString()));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
activity.RecordFailure(ex, null, (ActorRuntimeOpenTelemetryConsts.Message.Status, "failed"));
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public ActorResponseHandle SendRequest(ActorRequest request)
|
||||
{
|
||||
using var activity = ActivitySource.StartActivity(
|
||||
ActorRuntimeOpenTelemetryConsts.SpanNames.FormatRequestOperation(ActorRuntimeOpenTelemetryConsts.Operations.ProcessRequest));
|
||||
|
||||
activity.SetupRequestOperation(this.ActorId, request.MessageId, request.Method, "ActorContext", "SendRequest");
|
||||
|
||||
Log.SendRequestStarted(this._logger, this.ActorId.ToString(), request.MessageId);
|
||||
|
||||
try
|
||||
{
|
||||
lock (this._lock)
|
||||
{
|
||||
string requestStatus;
|
||||
if (!this._inbox.TryGetValue(request.MessageId, out var entry))
|
||||
{
|
||||
var requestMessage = new ActorRequestMessage(request.MessageId)
|
||||
{
|
||||
Method = request.Method,
|
||||
Params = request.Params
|
||||
};
|
||||
|
||||
entry = this._inbox[request.MessageId] = new(requestMessage);
|
||||
this._pendingMessages.Writer.TryWrite(requestMessage);
|
||||
Log.RequestMessageCreated(this._logger, this.ActorId.ToString(), request.MessageId, request.Method);
|
||||
requestStatus = "created";
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.RequestMessageFound(this._logger, this.ActorId.ToString(), request.MessageId);
|
||||
requestStatus = "found";
|
||||
}
|
||||
|
||||
var handle = new InProcessActorResponseHandle(this, entry);
|
||||
Log.ResponseHandleCreated(this._logger, this.ActorId.ToString(), request.MessageId);
|
||||
|
||||
activity.Complete(RequestCompleted, this.ActorId, [(Tel.Request.Status, requestStatus), (Tel.Response.Status, HandleCreated)],
|
||||
(Tel.Message.Id, request.MessageId), (Tel.Message.Method, request.Method));
|
||||
|
||||
return handle;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
activity.RecordFailure(ex, null, (ActorRuntimeOpenTelemetryConsts.Request.Status, "failed"));
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public void OnProgressUpdate(string messageId, int sequenceNumber, JsonElement data)
|
||||
{
|
||||
using var activity = ActivitySource.StartActivity(
|
||||
ActorRuntimeOpenTelemetryConsts.SpanNames.FormatActorOperation(ActorRuntimeOpenTelemetryConsts.Operations.ProgressUpdate));
|
||||
|
||||
activity.SetActorAttributes(this.ActorId);
|
||||
activity.SetMessageAttributes(messageId);
|
||||
activity?.SetTag(ActorRuntimeOpenTelemetryConsts.Message.SequenceNumber, sequenceNumber);
|
||||
|
||||
try
|
||||
{
|
||||
Log.ProgressUpdateReceived(this._logger, this.ActorId.ToString(), messageId, sequenceNumber);
|
||||
var update = new UpdateRequestOperation(messageId, RequestStatus.Pending, data);
|
||||
this.PostRequestUpdate(update);
|
||||
|
||||
activity.RecordSuccess((ActorRuntimeOpenTelemetryConsts.Message.Status, "processed"));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
activity.RecordFailure(ex);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private void PostRequestUpdate(UpdateRequestOperation update)
|
||||
{
|
||||
lock (this._lock)
|
||||
{
|
||||
if (!this._inbox.TryGetValue(update.MessageId, out var entry))
|
||||
{
|
||||
Log.ProgressUpdateFailed(this._logger, this.ActorId.ToString(), update.MessageId, "Message not found in inbox");
|
||||
throw new InvalidOperationException($"Message with id '{update.MessageId}' not found while publishing update.");
|
||||
}
|
||||
|
||||
entry.PostUpdate(update);
|
||||
if (update.Status is RequestStatus.Completed or RequestStatus.Failed)
|
||||
{
|
||||
entry.SetResponse(new ActorResponseMessage(update.MessageId)
|
||||
{
|
||||
SenderId = this.ActorId,
|
||||
Status = update.Status,
|
||||
Data = update.Data
|
||||
});
|
||||
}
|
||||
|
||||
Log.ProgressUpdatePublished(this._logger, this.ActorId.ToString(), update.MessageId);
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask<ReadResponse> ReadAsync(ActorReadOperationBatch operations, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Log.ReadOperationStarted(this._logger, this.ActorId.ToString(), operations.Operations.Count);
|
||||
var result = await this.Storage.ReadStateAsync(
|
||||
this.ActorId,
|
||||
[.. operations.Operations.OfType<ActorStateReadOperation>()],
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
Log.ReadOperationCompleted(this._logger, this.ActorId.ToString(), result.Results.Count);
|
||||
return result;
|
||||
}
|
||||
|
||||
public async IAsyncEnumerable<ActorMessage> WatchMessagesAsync([EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
Log.WatchMessagesStarted(this._logger, this.ActorId.ToString());
|
||||
|
||||
// TODO: Yield all pending requests - this likely requires reading the inbox from storage.
|
||||
// TODO: Yield all responses
|
||||
// TODO: Yield all updates
|
||||
var messageCount = 0;
|
||||
await foreach (var message in this._pendingMessages.Reader.ReadAllAsync(cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
messageCount++;
|
||||
var messageId = message switch
|
||||
{
|
||||
ActorRequestMessage requestMessage => requestMessage.MessageId,
|
||||
ActorResponseMessage responseMessage => responseMessage.MessageId,
|
||||
_ => "unknown"
|
||||
};
|
||||
Log.MessageYielded(this._logger, this.ActorId.ToString(), messageId, message.Type.ToString(), messageCount);
|
||||
yield return message;
|
||||
}
|
||||
|
||||
Log.WatchMessagesCompleted(this._logger, this.ActorId.ToString(), messageCount);
|
||||
}
|
||||
|
||||
public async ValueTask<WriteResponse> WriteAsync(ActorWriteOperationBatch operations, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Log.WriteOperationStarted(this._logger, this.ActorId.ToString(), operations.Operations.Count);
|
||||
|
||||
// TODO: Turn send & update message operations into storage writes to outbox
|
||||
|
||||
var result = await this.Storage.WriteStateAsync(
|
||||
this.ActorId,
|
||||
[.. operations.Operations.OfType<ActorStateWriteOperation>()],
|
||||
operations.ETag,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
Log.WriteOperationCompleted(this._logger, this.ActorId.ToString(), result.Success);
|
||||
|
||||
// Check if result success and schedule durable task to pump outbox if needed.
|
||||
if (result.Success)
|
||||
{
|
||||
var processedOperations = 0;
|
||||
foreach (var operation in operations.Operations)
|
||||
{
|
||||
if (operation is SendRequestOperation sendRequestOperation)
|
||||
{
|
||||
Log.SendRequestOperationEncountered(this._logger, this.ActorId.ToString());
|
||||
// Get the target actor from the runtime.
|
||||
// Enqueue the request on the actor's inbox.
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
else if (operation is UpdateRequestOperation updateRequestOperation)
|
||||
{
|
||||
Log.UpdateRequestOperationProcessing(this._logger, this.ActorId.ToString(), updateRequestOperation.MessageId);
|
||||
// Find the request in this actor's inbox.
|
||||
// Get the SenderId from the request.
|
||||
// Get the sending actor from the runtime.
|
||||
// Enqueue the request on the actor's inbox.
|
||||
this.PostRequestUpdate(updateRequestOperation);
|
||||
processedOperations++;
|
||||
}
|
||||
}
|
||||
Log.OperationProcessingCompleted(this._logger, this.ActorId.ToString(), processedOperations);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
Log.ActorContextDisposing(this._logger, this.ActorId.ToString());
|
||||
|
||||
this._cts.Dispose();
|
||||
await this._actorInstance.DisposeAsync().ConfigureAwait(false);
|
||||
if (this._actorRunTask is { } actorRunTask)
|
||||
{
|
||||
await actorRunTask.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
Log.ActorContextDisposed(this._logger, this.ActorId.ToString());
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Log.ActorContextDisposing(this._logger, this.ActorId.ToString());
|
||||
|
||||
this._cts.Dispose();
|
||||
#pragma warning disable CA2012 // Use ValueTasks correctly
|
||||
#pragma warning disable VSTHRD002 // Avoid problematic synchronous waits
|
||||
if (this._actorInstance is IDisposable actorInstanceDisposable)
|
||||
{
|
||||
actorInstanceDisposable.Dispose();
|
||||
}
|
||||
else
|
||||
{
|
||||
this._actorInstance.DisposeAsync().GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
this._actorRunTask?.GetAwaiter().GetResult();
|
||||
#pragma warning restore VSTHRD002 // Avoid problematic synchronous waits
|
||||
#pragma warning restore CA2012 // Use ValueTasks correctly
|
||||
|
||||
Log.ActorContextDisposed(this._logger, this.ActorId.ToString());
|
||||
}
|
||||
|
||||
private sealed class ActorInboxEntry(ActorRequestMessage Request)
|
||||
{
|
||||
private readonly TaskCompletionSource<ActorResponseMessage> _responseTcs = new(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
private readonly Channel<UpdateRequestOperation> _updates = Channel.CreateUnbounded<UpdateRequestOperation>();
|
||||
public CancellationTokenSource Cts { get; } = new();
|
||||
public ActorRequestMessage Request { get; } = Request;
|
||||
public Task<ActorResponseMessage> Response => this._responseTcs.Task;
|
||||
|
||||
public IAsyncEnumerable<UpdateRequestOperation> WatchUpdatesAsync(CancellationToken cancellationToken)
|
||||
=> this._updates.Reader.ReadAllAsync(cancellationToken);
|
||||
|
||||
public void PostUpdate(UpdateRequestOperation update)
|
||||
{
|
||||
if (!this._updates.Writer.TryWrite(update))
|
||||
{
|
||||
throw new InvalidOperationException("Failed to write update to the channel.");
|
||||
}
|
||||
}
|
||||
|
||||
public void SetResponse(ActorResponseMessage response)
|
||||
{
|
||||
if (!this._responseTcs.TrySetResult(response))
|
||||
{
|
||||
throw new InvalidOperationException("Response has already been set.");
|
||||
}
|
||||
|
||||
this._updates.Writer.TryComplete();
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class InProcessActorResponseHandle(InProcessActorContext context, ActorInboxEntry entry) : ActorResponseHandle
|
||||
{
|
||||
#if NET8_0_OR_GREATER
|
||||
public override async ValueTask CancelAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await entry.Cts.CancelAsync().ConfigureAwait(false);
|
||||
}
|
||||
#else
|
||||
public override ValueTask CancelAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
entry.Cts.Cancel();
|
||||
return default;
|
||||
}
|
||||
#endif
|
||||
|
||||
public override async ValueTask<ActorResponse> GetResponseAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
ActorResponse response;
|
||||
#pragma warning disable CA1031 // Do not catch general exception types
|
||||
try
|
||||
{
|
||||
var responseMessage = await entry.Response
|
||||
#if NET8_0_OR_GREATER
|
||||
.WaitAsync(cancellationToken)
|
||||
#endif
|
||||
.ConfigureAwait(false);
|
||||
response = new ActorResponse
|
||||
{
|
||||
ActorId = context.ActorId,
|
||||
MessageId = entry.Request.MessageId,
|
||||
Data = responseMessage.Data,
|
||||
Status = responseMessage.Status,
|
||||
};
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
response = new ActorResponse
|
||||
{
|
||||
ActorId = context.ActorId,
|
||||
MessageId = entry.Request.MessageId,
|
||||
Data = JsonSerializer.SerializeToElement($"Error: {exception.Message}", context._runtime.JsonSerializerOptions.GetTypeInfo<string>(ActorRuntimeJsonContext.Default)),
|
||||
Status = RequestStatus.Failed,
|
||||
};
|
||||
}
|
||||
#pragma warning restore CA1031 // Do not catch general exception types
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
public override bool TryGetResponse([NotNullWhen(true)] out ActorResponse? response)
|
||||
{
|
||||
if (entry.Response.Status is TaskStatus.RanToCompletion)
|
||||
{
|
||||
#pragma warning disable VSTHRD002 // Avoid problematic synchronous waits
|
||||
var responseMessage = entry.Response.GetAwaiter().GetResult();
|
||||
#pragma warning restore VSTHRD002 // Avoid problematic synchronous waits
|
||||
response = new ActorResponse
|
||||
{
|
||||
ActorId = context.ActorId,
|
||||
MessageId = entry.Request.MessageId,
|
||||
Data = responseMessage.Data,
|
||||
Status = responseMessage.Status,
|
||||
};
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
response = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
public override async IAsyncEnumerable<ActorRequestUpdate> WatchUpdatesAsync([EnumeratorCancellation] CancellationToken cancellationToken)
|
||||
{
|
||||
await foreach (var update in entry.WatchUpdatesAsync(cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
yield return new ActorRequestUpdate(update.Status, update.Data);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.Metrics;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using static Microsoft.Extensions.AI.Agents.Runtime.ActivityExtensions;
|
||||
using Tel = Microsoft.Extensions.AI.Agents.Runtime.ActorRuntimeOpenTelemetryConsts;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.Runtime;
|
||||
|
||||
internal sealed class InProcessActorRuntime(
|
||||
IServiceProvider serviceProvider,
|
||||
IReadOnlyDictionary<ActorType, Func<IServiceProvider, IActorRuntimeContext, IActor>> actorFactories,
|
||||
IActorStateStorage storage,
|
||||
JsonSerializerOptions jsonSerializerOptions)
|
||||
{
|
||||
private static readonly ActivitySource ActivitySource = new(ActorRuntimeOpenTelemetryConsts.InProcessSourceName);
|
||||
private static readonly Meter Meter = new(ActorRuntimeOpenTelemetryConsts.InProcessSourceName);
|
||||
|
||||
// Metrics following OpenTelemetry semantic conventions
|
||||
private static readonly Counter<long> ActorCreatedCounter = Meter.CreateCounter<long>(
|
||||
ActorRuntimeOpenTelemetryConsts.Client.ActorCount.Name,
|
||||
ActorRuntimeOpenTelemetryConsts.CountUnit,
|
||||
ActorRuntimeOpenTelemetryConsts.Client.ActorCount.Description);
|
||||
|
||||
private static readonly Histogram<double> OperationDurationHistogram = Meter.CreateHistogram<double>(
|
||||
ActorRuntimeOpenTelemetryConsts.Client.OperationDuration.Name,
|
||||
"s",
|
||||
ActorRuntimeOpenTelemetryConsts.Client.OperationDuration.Description);
|
||||
|
||||
private readonly object _createActorLock = new();
|
||||
private readonly IReadOnlyDictionary<ActorType, Func<IServiceProvider, IActorRuntimeContext, IActor>> _actorFactories = actorFactories;
|
||||
private readonly ConcurrentDictionary<ActorId, InProcessActorContext> _actors = [];
|
||||
|
||||
public IActorStateStorage Storage { get; } = storage;
|
||||
public JsonSerializerOptions JsonSerializerOptions { get; } = jsonSerializerOptions;
|
||||
public IServiceProvider Services { get; } = serviceProvider;
|
||||
|
||||
internal InProcessActorContext GetOrCreateActor(ActorId actorId)
|
||||
{
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
|
||||
// Create span following OpenTelemetry conventions for RPC operations
|
||||
using var activity = ActivitySource.StartActivity(
|
||||
ActorRuntimeOpenTelemetryConsts.SpanNames.FormatActorOperation(ActorRuntimeOpenTelemetryConsts.Operations.GetActor));
|
||||
|
||||
try
|
||||
{
|
||||
if (this._actors.TryGetValue(actorId, out var context))
|
||||
{
|
||||
activity.SetupActorOperation(actorId, exists: true);
|
||||
activity.Event(ActorStarted, actorId);
|
||||
return context;
|
||||
}
|
||||
|
||||
if (!this._actorFactories.TryGetValue(actorId.Type, out var factory))
|
||||
{
|
||||
var errorMessage = $"No factory registered for actor type '{actorId.Type}'";
|
||||
var exception = new InvalidOperationException(errorMessage);
|
||||
|
||||
activity.SetupActorOperation(actorId, exists: false);
|
||||
activity.RecordFailure(exception, ActorRuntimeOpenTelemetryConsts.ErrorInfo.Types.ActorNotFound);
|
||||
throw exception;
|
||||
}
|
||||
|
||||
if (!this._actors.TryGetValue(actorId, out var actorContext))
|
||||
{
|
||||
#if NETSTANDARD
|
||||
InProcessActorContext ValueFactory(ActorId actorId)
|
||||
{
|
||||
var self = this;
|
||||
|
||||
return CreateActorInstance(actorId, self, factory);
|
||||
}
|
||||
|
||||
actorContext = this._actors.GetOrAdd(actorId, ValueFactory);
|
||||
#else
|
||||
static InProcessActorContext ValueFactory(
|
||||
ActorId actorId,
|
||||
(InProcessActorRuntime, Func<IServiceProvider, IActorRuntimeContext, IActor>) state)
|
||||
{
|
||||
var (self, factory) = state;
|
||||
return CreateActorInstance(actorId, self, factory);
|
||||
}
|
||||
|
||||
actorContext = this._actors.GetOrAdd(actorId, ValueFactory, (this, factory));
|
||||
#endif
|
||||
}
|
||||
|
||||
activity.SetupActorOperation(actorId, exists: false);
|
||||
activity.RecordSuccess();
|
||||
return actorContext;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
activity.RecordFailure(ex);
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Record operation duration metric
|
||||
var duration = stopwatch.Elapsed.TotalSeconds;
|
||||
OperationDurationHistogram.Record(duration,
|
||||
new KeyValuePair<string, object?>(ActorRuntimeOpenTelemetryConsts.Actor.Operation, ActorRuntimeOpenTelemetryConsts.Operations.GetActor),
|
||||
new KeyValuePair<string, object?>(ActorRuntimeOpenTelemetryConsts.Actor.Type, actorId.Type.Name));
|
||||
}
|
||||
}
|
||||
|
||||
private static InProcessActorContext CreateActorInstance(ActorId actorId, InProcessActorRuntime self, Func<IServiceProvider, IActorRuntimeContext, IActor> factory)
|
||||
{
|
||||
lock (self._createActorLock)
|
||||
{
|
||||
// Create nested span for actor creation
|
||||
var createActivity = ActivitySource.StartActivity(
|
||||
ActorRuntimeOpenTelemetryConsts.SpanNames.FormatActorOperation(ActorRuntimeOpenTelemetryConsts.Operations.CreateActor));
|
||||
InProcessActorContext? instance = null;
|
||||
try
|
||||
{
|
||||
createActivity.SetupActorOperation(actorId);
|
||||
|
||||
instance = new InProcessActorContext(actorId, self, factory);
|
||||
instance.Start();
|
||||
|
||||
createActivity.Complete(ActorCreated, actorId, [(Tel.Actor.Started, true)]);
|
||||
|
||||
// Record metrics for successful actor creation
|
||||
ActorCreatedCounter.Add(1, new KeyValuePair<string, object?>(ActorRuntimeOpenTelemetryConsts.Actor.Type, actorId.Type.Name));
|
||||
return instance;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
instance?.Dispose();
|
||||
createActivity.RecordFailure(ex);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class InProcessActorClient(InProcessActorRuntime runtime) : IActorClient
|
||||
{
|
||||
private static readonly ActivitySource ActivitySource = new(ActorRuntimeOpenTelemetryConsts.InProcessSourceName);
|
||||
private static readonly Meter ClientMeter = new(ActorRuntimeOpenTelemetryConsts.InProcessSourceName);
|
||||
private static readonly Counter<long> RequestCounter = ClientMeter.CreateCounter<long>(
|
||||
ActorRuntimeOpenTelemetryConsts.Client.RequestCount.Name,
|
||||
ActorRuntimeOpenTelemetryConsts.CountUnit,
|
||||
ActorRuntimeOpenTelemetryConsts.Client.RequestCount.Description);
|
||||
private static readonly Histogram<double> ClientOperationDurationHistogram = ClientMeter.CreateHistogram<double>(
|
||||
ActorRuntimeOpenTelemetryConsts.Client.OperationDuration.Name,
|
||||
"s",
|
||||
ActorRuntimeOpenTelemetryConsts.Client.OperationDuration.Description);
|
||||
|
||||
private readonly InProcessActorRuntime _runtime = runtime;
|
||||
|
||||
public ValueTask<ActorResponseHandle> GetResponseAsync(ActorId actorId, string messageId, CancellationToken cancellationToken)
|
||||
{
|
||||
// Create span for get response operation
|
||||
using var activity = ActivitySource.StartActivity(
|
||||
ActorRuntimeOpenTelemetryConsts.SpanNames.FormatRequestOperation(ActorRuntimeOpenTelemetryConsts.Operations.ReceiveResponse));
|
||||
|
||||
activity.SetupRequestOperation(actorId, messageId, service: "ActorClient", rpcMethod: "GetResponse");
|
||||
|
||||
throw new NotImplementedException("GetResponseAsync is not yet implemented");
|
||||
}
|
||||
|
||||
public ValueTask<ActorResponseHandle> SendRequestAsync(ActorRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
|
||||
// Create span for send request operation following RPC client conventions
|
||||
using var activity = ActivitySource.StartActivity(
|
||||
ActorRuntimeOpenTelemetryConsts.SpanNames.FormatRequestOperation(ActorRuntimeOpenTelemetryConsts.Operations.SendRequest));
|
||||
|
||||
try
|
||||
{
|
||||
activity.SetupRequestOperation(request.ActorId, request.MessageId, request.Method);
|
||||
|
||||
// Ensure the message is enqueued on the actor's inbox, getting a response handle for it.
|
||||
var actorId = request.ActorId;
|
||||
var actorContext = this._runtime.GetOrCreateActor(actorId);
|
||||
var response = actorContext.SendRequest(request);
|
||||
|
||||
activity.Complete(MessageSent, actorId, Sent, (Tel.Message.Id, request.MessageId));
|
||||
|
||||
// Record request metric
|
||||
RequestCounter.Add(1,
|
||||
new KeyValuePair<string, object?>(Tel.Actor.Type, actorId.Type.Name),
|
||||
new KeyValuePair<string, object?>(Tel.Message.Method, request.Method));
|
||||
|
||||
return new(response);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
activity.RecordFailure(ex, null, (ActorRuntimeOpenTelemetryConsts.Request.Status, "failed"));
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Record operation duration
|
||||
var duration = stopwatch.Elapsed.TotalSeconds;
|
||||
ClientOperationDurationHistogram.Record(duration,
|
||||
new KeyValuePair<string, object?>(ActorRuntimeOpenTelemetryConsts.Actor.Operation, ActorRuntimeOpenTelemetryConsts.Operations.SendRequest),
|
||||
new KeyValuePair<string, object?>(ActorRuntimeOpenTelemetryConsts.Actor.Type, request.ActorId.Type.Name));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Text.Json.Serialization.Metadata;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// Provides extension methods for JSON serialization with source generation support.
|
||||
/// </summary>
|
||||
internal static class JsonSerializerExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the JsonTypeInfo for a type, preferring the one from options if available,
|
||||
/// otherwise falling back to the source-generated context.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type to get JsonTypeInfo for.</typeparam>
|
||||
/// <param name="options">The JsonSerializerOptions to check first.</param>
|
||||
/// <param name="fallbackContext">The fallback JsonSerializerContext to use if not found in options.</param>
|
||||
/// <returns>The JsonTypeInfo for the requested type.</returns>
|
||||
public static JsonTypeInfo<T> GetTypeInfo<T>(this JsonSerializerOptions options, JsonSerializerContext fallbackContext)
|
||||
{
|
||||
// Try to get from the options first (if a context is configured)
|
||||
if (options.TypeInfoResolver?.GetTypeInfo(typeof(T), options) is JsonTypeInfo<T> typeInfo)
|
||||
{
|
||||
return typeInfo;
|
||||
}
|
||||
|
||||
// Fall back to the provided source-generated context
|
||||
return (JsonTypeInfo<T>)fallbackContext.GetTypeInfo(typeof(T))!;
|
||||
}
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>$(ProjectsTargetFrameworks)</TargetFrameworks>
|
||||
<TargetFrameworks Condition="'$(Configuration)' == 'Debug'">$(ProjectsDebugTargetFrameworks)</TargetFrameworks>
|
||||
<NoWarn>$(NoWarn);IDE1006;IDE0130</NoWarn>
|
||||
<VersionSuffix>alpha</VersionSuffix>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<InjectSharedThrow>true</InjectSharedThrow>
|
||||
<InjectDiagnosticAttributesOnLegacy>true</InjectDiagnosticAttributesOnLegacy>
|
||||
<InjectTrimAttributesOnLegacy>true</InjectTrimAttributesOnLegacy>
|
||||
<InjectIsExternalInitOnLegacy>true</InjectIsExternalInitOnLegacy>
|
||||
</PropertyGroup>
|
||||
|
||||
<Import Project="$(RepoRoot)/dotnet/nuget/nuget-package.props" />
|
||||
|
||||
<ItemGroup>
|
||||
<InternalsVisibleTo Include="Microsoft.Extensions.AI.Agents.Runtime.Abstractions.UnitTests" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(TargetFrameworkIdentifier)' != '.NETCoreApp'">
|
||||
<PackageReference Include="Microsoft.Bcl.HashCode" />
|
||||
<PackageReference Include="System.Text.Json" />
|
||||
<PackageReference Include="System.Threading.Channels" />
|
||||
<PackageReference Include="System.Threading.Tasks.Extensions" />
|
||||
<PackageReference Include="System.Diagnostics.DiagnosticSource" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Microsoft.Extensions.AI.Agents.Runtime.Abstractions\Microsoft.Extensions.AI.Agents.Runtime.Abstractions.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,20 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents;
|
||||
|
||||
/// <summary>
|
||||
/// Source-generated JSON type information for use by all Agents implementations.
|
||||
/// </summary>
|
||||
[JsonSourceGenerationOptions(
|
||||
JsonSerializerDefaults.Web,
|
||||
UseStringEnumConverter = true,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
WriteIndented = false)]
|
||||
[JsonSerializable(typeof(ChatMessage))]
|
||||
[JsonSerializable(typeof(List<ChatMessage>))]
|
||||
[JsonSerializable(typeof(ChatClientAgentThread))]
|
||||
internal sealed partial class AgentsJsonContext : JsonSerializerContext;
|
||||
@@ -1,8 +1,12 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
@@ -12,6 +16,7 @@ namespace Microsoft.Extensions.AI.Agents;
|
||||
/// <summary>
|
||||
/// Chat client agent thread.
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(Converter))]
|
||||
public sealed class ChatClientAgentThread : AgentThread, IMessagesRetrievableThread
|
||||
{
|
||||
private readonly List<ChatMessage> _chatMessages = [];
|
||||
@@ -89,4 +94,92 @@ public sealed class ChatClientAgentThread : AgentThread, IMessagesRetrievableThr
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Provides a <see cref="JsonConverter"/> for <see cref="ChatClientAgentThread"/> objects.
|
||||
/// </summary>
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public sealed class Converter : JsonConverter<ChatClientAgentThread>
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public override ChatClientAgentThread? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
if (reader.TokenType != JsonTokenType.StartObject)
|
||||
{
|
||||
throw new JsonException("Expected StartObject token");
|
||||
}
|
||||
|
||||
using var doc = JsonDocument.ParseValue(ref reader);
|
||||
var root = doc.RootElement;
|
||||
|
||||
// Extract properties from JSON
|
||||
string? id = null;
|
||||
if (root.TryGetProperty("id", out var idProperty))
|
||||
{
|
||||
id = idProperty.GetString();
|
||||
}
|
||||
|
||||
List<ChatMessage>? messages = null;
|
||||
if (root.TryGetProperty("messages", out var messagesProperty))
|
||||
{
|
||||
if (messagesProperty.ValueKind == JsonValueKind.Array)
|
||||
{
|
||||
messages = [];
|
||||
foreach (var messageElement in messagesProperty.EnumerateArray())
|
||||
{
|
||||
var message = messageElement.Deserialize(options.GetTypeInfo<ChatMessage>(AgentsJsonContext.Default));
|
||||
if (message != null)
|
||||
{
|
||||
messages.Add(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create the appropriate instance based on available data
|
||||
// StorageLocation will be set automatically by the constructors
|
||||
ChatClientAgentThread thread;
|
||||
if (messages?.Count > 0)
|
||||
{
|
||||
thread = new ChatClientAgentThread(messages);
|
||||
}
|
||||
else if (!string.IsNullOrWhiteSpace(id))
|
||||
{
|
||||
thread = new ChatClientAgentThread(id);
|
||||
}
|
||||
else
|
||||
{
|
||||
thread = new ChatClientAgentThread();
|
||||
}
|
||||
|
||||
// Override Id if it was explicitly set in JSON (for cases where messages exist but ID is also provided)
|
||||
if (id != null)
|
||||
{
|
||||
thread.Id = id;
|
||||
}
|
||||
|
||||
return thread;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Write(Utf8JsonWriter writer, ChatClientAgentThread value, JsonSerializerOptions options)
|
||||
{
|
||||
writer.WriteStartObject();
|
||||
|
||||
// Write base properties
|
||||
if (value.Id != null)
|
||||
{
|
||||
writer.WriteString("id", value.Id);
|
||||
}
|
||||
|
||||
// Write messages if in memory storage (StorageLocation is determined by presence of messages vs ID)
|
||||
if (value.StorageLocation == ChatClientAgentThreadType.InMemoryMessages)
|
||||
{
|
||||
writer.WritePropertyName("messages");
|
||||
JsonSerializer.Serialize(writer, value._chatMessages, options.GetTypeInfo<List<ChatMessage>>(AgentsJsonContext.Default));
|
||||
}
|
||||
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Text.Json.Serialization.Metadata;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents;
|
||||
|
||||
/// <summary>
|
||||
/// Provides extension methods for JSON serialization with source generation support.
|
||||
/// </summary>
|
||||
internal static class JsonSerializerExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the JsonTypeInfo for a type, preferring the one from options if available,
|
||||
/// otherwise falling back to the source-generated context.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type to get JsonTypeInfo for.</typeparam>
|
||||
/// <param name="options">The JsonSerializerOptions to check first.</param>
|
||||
/// <param name="fallbackContext">The fallback JsonSerializerContext to use if not found in options.</param>
|
||||
/// <returns>The JsonTypeInfo for the requested type.</returns>
|
||||
public static JsonTypeInfo<T> GetTypeInfo<T>(this JsonSerializerOptions options, JsonSerializerContext fallbackContext)
|
||||
{
|
||||
// Try to get from the options first (if a context is configured)
|
||||
if (options.TypeInfoResolver?.GetTypeInfo(typeof(T), options) is JsonTypeInfo<T> typeInfo)
|
||||
{
|
||||
return typeInfo;
|
||||
}
|
||||
|
||||
// Fall back to the provided source-generated context
|
||||
return (JsonTypeInfo<T>)fallbackContext.GetTypeInfo(typeof(T))!;
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@
|
||||
<PropertyGroup>
|
||||
<InjectSharedThrow>true</InjectSharedThrow>
|
||||
<InjectDiagnosticClassesOnLegacy>true</InjectDiagnosticClassesOnLegacy>
|
||||
<InjectTrimAttributesOnLegacy>true</InjectTrimAttributesOnLegacy>
|
||||
</PropertyGroup>
|
||||
|
||||
<Import Project="$(RepoRoot)/dotnet/nuget/nuget-package.props" />
|
||||
|
||||
Reference in New Issue
Block a user