Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,6 @@ dotnet_diagnostic.RCS1097.severity = warning # Remove redundant 'ToString
dotnet_diagnostic.RCS1192.severity = warning # Unnecessary usage of verbatim string literal.
dotnet_diagnostic.RCS1211.severity = warning # Remove unnecessary else clause.
dotnet_diagnostic.RCS1214.severity = warning # Unnecessary interpolated string.
dotnet_diagnostic.RCS1225.severity = warning # Make class sealed.
dotnet_diagnostic.RCS1225.severity = warning # Make class sealed.
dotnet_diagnostic.RCS1141.severity = warning # Add documentation comment to public members.
dotnet_diagnostic.RCS1232.severity = warning # Order elements in documentation comment.
1 change: 1 addition & 0 deletions src/A2A.AspNetCore/A2A.AspNetCore.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
<PackageTags>Agent2Agent;a2a;agent;ai;llm;aspnetcore</PackageTags>
<PackageReadmeFile>README.md</PackageReadmeFile>
<IsPackable>true</IsPackable>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>

<ItemGroup>
Expand Down
24 changes: 15 additions & 9 deletions src/A2A.AspNetCore/A2AEndpointRouteBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,23 @@

namespace A2A.AspNetCore;

/// <summary>
/// Extension methods for configuring A2A endpoints in ASP.NET Core applications.
/// </summary>
public static class A2ARouteBuilderExtensions
{
/// <summary>
/// Activity source for tracing A2A endpoint operations.
/// </summary>
public static readonly ActivitySource ActivitySource = new("A2A.Endpoint", "1.0.0");

/// <summary>
/// Enables JSONRPC A2A endpoints for the specified path.
/// Enables JSON-RPC A2A endpoints for the specified path.
/// </summary>
/// <param name="endpoints"></param>
/// <param name="taskManager"></param>
/// <param name="path"></param>
/// <returns></returns>
/// <param name="endpoints">The endpoint route builder to configure.</param>
/// <param name="taskManager">The task manager for handling A2A operations.</param>
/// <param name="path">The base path for the A2A endpoints.</param>
/// <returns>An endpoint convention builder for further configuration.</returns>
public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpoints, TaskManager taskManager, string path)
{
var loggerFactory = endpoints.ServiceProvider.GetRequiredService<ILoggerFactory>();
Expand All @@ -41,10 +47,10 @@ public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpo
/// <summary>
/// Enables experimental HTTP A2A endpoints for the specified path.
/// </summary>
/// <param name="endpoints"></param>
/// <param name="taskManager"></param>
/// <param name="path"></param>
/// <returns></returns>
/// <param name="endpoints">The endpoint route builder to configure.</param>
/// <param name="taskManager">The task manager for handling A2A operations.</param>
/// <param name="path">The base path for the HTTP A2A endpoints.</param>
/// <returns>An endpoint convention builder for further configuration.</returns>
public static IEndpointConventionBuilder MapHttpA2A(this IEndpointRouteBuilder endpoints, TaskManager taskManager, string path)
{
var loggerFactory = endpoints.ServiceProvider.GetRequiredService<ILoggerFactory>();
Expand Down
142 changes: 142 additions & 0 deletions src/A2A.AspNetCore/A2AHttpProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,30 @@

namespace A2A.AspNetCore;

/// <summary>
/// Static processor class for handling A2A HTTP requests in ASP.NET Core applications.
/// </summary>
/// <remarks>
/// Provides methods for processing agent card queries, task operations, message sending,
/// and push notification configuration through HTTP endpoints.
/// </remarks>
public static class A2AHttpProcessor
{
/// <summary>
/// OpenTelemetry ActivitySource for tracing HTTP processor operations.
/// </summary>
public static readonly ActivitySource ActivitySource = new("A2A.HttpProcessor", "1.0.0");

/// <summary>
/// Processes a request to retrieve the agent card containing agent capabilities and metadata.
/// </summary>
/// <remarks>
/// Invokes the task manager's agent card query handler to get current agent information.
/// </remarks>
/// <param name="taskManager">The task manager instance containing the agent card query handler.</param>
/// <param name="logger">Logger instance for recording operation details and errors.</param>
/// <param name="agentUrl">The URL of the agent to retrieve the card for.</param>
/// <returns>An HTTP result containing the agent card JSON or an error response.</returns>
internal static Task<IResult> GetAgentCard(TaskManager taskManager, ILogger logger, string agentUrl)
{
using var activity = ActivitySource.StartActivity("GetAgentCard", ActivityKind.Server);
Expand All @@ -27,6 +47,18 @@ internal static Task<IResult> GetAgentCard(TaskManager taskManager, ILogger logg
}
}

/// <summary>
/// Processes a request to retrieve a specific task by its ID.
/// </summary>
/// <remarks>
/// Returns the task's current state, history, and metadata with optional history length limiting.
/// </remarks>
/// <param name="taskManager">The task manager instance for accessing task storage.</param>
/// <param name="logger">Logger instance for recording operation details and errors.</param>
/// <param name="id">The unique identifier of the task to retrieve.</param>
/// <param name="historyLength">Optional limit on the number of history items to return.</param>
/// <param name="metadata">Optional JSON metadata filter for the task query.</param>
/// <returns>An HTTP result containing the task JSON or a not found/error response.</returns>
internal static async Task<IResult> GetTask(TaskManager taskManager, ILogger logger, string id, int? historyLength, string? metadata)
{
using var activity = ActivitySource.StartActivity("GetTask", ActivityKind.Server);
Expand All @@ -50,6 +82,16 @@ internal static async Task<IResult> GetTask(TaskManager taskManager, ILogger log
}
}

/// <summary>
/// Processes a request to cancel a specific task by setting its status to Canceled.
/// </summary>
/// <remarks>
/// Invokes the task manager's cancellation logic and returns the updated task state.
/// </remarks>
/// <param name="taskManager">The task manager instance for handling task cancellation.</param>
/// <param name="logger">Logger instance for recording operation details and errors.</param>
/// <param name="id">The unique identifier of the task to cancel.</param>
/// <returns>An HTTP result containing the canceled task JSON or a not found/error response.</returns>
internal static async Task<IResult> CancelTask(TaskManager taskManager, ILogger logger, string id)
{
using var activity = ActivitySource.StartActivity("CancelTask", ActivityKind.Server);
Expand All @@ -72,6 +114,20 @@ internal static async Task<IResult> CancelTask(TaskManager taskManager, ILogger
}
}

/// <summary>
/// Processes a request to send a message to a task and return a single response.
/// </summary>
/// <remarks>
/// Creates a new task if no task ID is provided, or updates an existing task's history.
/// Configures message sending parameters including history length and metadata.
/// </remarks>
/// <param name="taskManager">The task manager instance for handling message processing.</param>
/// <param name="logger">Logger instance for recording operation details and errors.</param>
/// <param name="taskId">Optional task ID to send the message to. If null, a new task may be created.</param>
/// <param name="sendParams">The message parameters containing the message content and configuration.</param>
/// <param name="historyLength">Optional limit on the number of history items to include in processing.</param>
/// <param name="metadata">Optional JSON metadata to include with the message request.</param>
/// <returns>An HTTP result containing the agent's response (Task or Message) or an error response.</returns>
internal static async Task<IResult> SendTaskMessage(TaskManager taskManager, ILogger logger, string? taskId, MessageSendParams sendParams, int? historyLength, string? metadata)
{
using var activity = ActivitySource.StartActivity("SendTaskMessage", ActivityKind.Server);
Expand Down Expand Up @@ -107,6 +163,20 @@ internal static async Task<IResult> SendTaskMessage(TaskManager taskManager, ILo
}
}

/// <summary>
/// Processes a request to send a message to a task and return a stream of events.
/// </summary>
/// <remarks>
/// Creates or updates a task and establishes a Server-Sent Events stream that yields
/// Task, Message, TaskStatusUpdateEvent, and TaskArtifactUpdateEvent objects as they occur.
/// </remarks>
/// <param name="taskManager">The task manager instance for handling streaming message processing.</param>
/// <param name="logger">Logger instance for recording operation details and errors.</param>
/// <param name="id">The unique identifier of the task to send the message to.</param>
/// <param name="sendParams">The message parameters containing the message content and configuration.</param>
/// <param name="historyLength">Optional limit on the number of history items to include in processing.</param>
/// <param name="metadata">Optional JSON metadata to include with the message request.</param>
/// <returns>An HTTP result that streams events as Server-Sent Events or an error response.</returns>
internal static async Task<IResult> SendSubscribeTaskMessage(TaskManager taskManager, ILogger logger, string id, MessageSendParams sendParams, int? historyLength, string? metadata)
{
using var activity = ActivitySource.StartActivity("SendSubscribeTaskMessage", ActivityKind.Server);
Expand All @@ -132,6 +202,17 @@ internal static async Task<IResult> SendSubscribeTaskMessage(TaskManager taskMan
}
}

/// <summary>
/// Processes a request to resubscribe to an existing task's event stream.
/// </summary>
/// <remarks>
/// Returns the active event enumerator for the specified task, allowing clients
/// to reconnect to ongoing task updates via Server-Sent Events.
/// </remarks>
/// <param name="taskManager">The task manager instance containing active task event streams.</param>
/// <param name="logger">Logger instance for recording operation details and errors.</param>
/// <param name="id">The unique identifier of the task to resubscribe to.</param>
/// <returns>An HTTP result that streams existing task events or an error response.</returns>
internal static IResult ResubscribeTask(TaskManager taskManager, ILogger logger, string id)
{
using var activity = ActivitySource.StartActivity("ResubscribeTask", ActivityKind.Server);
Expand All @@ -150,6 +231,17 @@ internal static IResult ResubscribeTask(TaskManager taskManager, ILogger logger,
}
}

/// <summary>
/// Processes a request to set or update push notification configuration for a specific task.
/// </summary>
/// <remarks>
/// Configures callback URLs and authentication settings for receiving task update notifications via HTTP.
/// </remarks>
/// <param name="taskManager">The task manager instance for handling push notification configuration.</param>
/// <param name="logger">Logger instance for recording operation details and errors.</param>
/// <param name="id">The unique identifier of the task to configure push notifications for.</param>
/// <param name="pushNotificationConfig">The push notification configuration containing callback URL and authentication details.</param>
/// <returns>An HTTP result containing the configured settings or an error response.</returns>
internal static async Task<IResult> SetPushNotification(TaskManager taskManager, ILogger logger, string id, PushNotificationConfig pushNotificationConfig)
{
using var activity = ActivitySource.StartActivity("ConfigurePushNotification", ActivityKind.Server);
Expand Down Expand Up @@ -178,6 +270,16 @@ internal static async Task<IResult> SetPushNotification(TaskManager taskManager,
}
}

/// <summary>
/// Processes a request to retrieve the push notification configuration for a specific task.
/// </summary>
/// <remarks>
/// Returns the callback URL and authentication settings configured for receiving task update notifications.
/// </remarks>
/// <param name="taskManager">The task manager instance for accessing push notification configurations.</param>
/// <param name="logger">Logger instance for recording operation details and errors.</param>
/// <param name="id">The unique identifier of the task to get push notification configuration for.</param>
/// <returns>An HTTP result containing the push notification configuration or a not found/error response.</returns>
internal static async Task<IResult> GetPushNotification(TaskManager taskManager, ILogger logger, string id)
{
using var activity = ActivitySource.StartActivity("GetPushNotification", ActivityKind.Server);
Expand All @@ -203,14 +305,34 @@ internal static async Task<IResult> GetPushNotification(TaskManager taskManager,
}
}

/// <summary>
/// Result type for returning A2A responses as JSON in HTTP responses.
/// </summary>
/// <remarks>
/// Implements IResult to provide custom serialization of A2AResponse objects
/// using the configured JSON serialization options.
/// </remarks>
public class A2AResponseResult : IResult
{
private readonly A2AResponse a2aResponse;

/// <summary>
/// Initializes a new instance of the A2AResponseResult class.
/// </summary>
/// <param name="a2aResponse">The A2A response object to serialize and return in the HTTP response.</param>
public A2AResponseResult(A2AResponse a2aResponse)
{
this.a2aResponse = a2aResponse;
}

/// <summary>
/// Executes the result by serializing the A2A response as JSON to the HTTP response body.
/// </summary>
/// <remarks>
/// Sets the appropriate content type and uses the default A2A JSON serialization options.
/// </remarks>
/// <param name="httpContext">The HTTP context to write the response to.</param>
/// <returns>A task representing the asynchronous serialization operation.</returns>
public async Task ExecuteAsync(HttpContext httpContext)
{
httpContext.Response.ContentType = "application/json";
Expand All @@ -219,15 +341,35 @@ public async Task ExecuteAsync(HttpContext httpContext)
}
}

/// <summary>
/// Result type for streaming A2A events as Server-Sent Events (SSE) in HTTP responses.
/// </summary>
/// <remarks>
/// Implements IResult to provide real-time streaming of task events including Task objects,
/// TaskStatusUpdateEvent, and TaskArtifactUpdateEvent objects.
/// </remarks>
public class A2AEventStreamResult : IResult
{
private readonly IAsyncEnumerable<A2AEvent> taskEvents;

/// <summary>
/// Initializes a new instance of the A2AEventStreamResult class.
/// </summary>
/// <param name="taskEvents">The async enumerable stream of A2A events to send as Server-Sent Events.</param>
public A2AEventStreamResult(IAsyncEnumerable<A2AEvent> taskEvents)
{
this.taskEvents = taskEvents;
}

/// <summary>
/// Executes the result by streaming A2A events as Server-Sent Events to the HTTP response.
/// </summary>
/// <remarks>
/// Sets the appropriate SSE content type and formats each event according to the SSE specification.
/// Each event is serialized as JSON and sent with the "data:" prefix followed by double newlines.
/// </remarks>
/// <param name="httpContext">The HTTP context to stream the events to.</param>
/// <returns>A task representing the asynchronous streaming operation.</returns>
public async Task ExecuteAsync(HttpContext httpContext)
{
httpContext.Response.ContentType = "text/event-stream";
Expand Down
Loading