diff --git a/reference/rest/src/main/java/io/a2a/server/rest/quarkus/A2AServerRoutes.java b/reference/rest/src/main/java/io/a2a/server/rest/quarkus/A2AServerRoutes.java index 9b6bf510a..350bc29bb 100644 --- a/reference/rest/src/main/java/io/a2a/server/rest/quarkus/A2AServerRoutes.java +++ b/reference/rest/src/main/java/io/a2a/server/rest/quarkus/A2AServerRoutes.java @@ -63,6 +63,61 @@ import static io.a2a.transport.rest.context.RestContextKeys.TENANT_KEY; +/** + * Quarkus reactive routes for A2A protocol REST endpoints. + * + *

This class defines all HTTP routes for the A2A protocol using Quarkus Reactive Routes + * (Vert.x web). Each method is annotated with {@code @Route} to map HTTP requests to + * A2A operations, delegating to {@link RestHandler} for processing. + * + *

Route Mapping

+ *

Routes support optional tenant prefixing and use regex patterns for flexible matching: + *

+ * POST   /{tenant}/message:send           → sendMessage()
+ * POST   /{tenant}/message:stream         → sendMessageStreaming()
+ * GET    /{tenant}/tasks                  → listTasks()
+ * GET    /{tenant}/tasks/{taskId}         → getTask()
+ * POST   /{tenant}/tasks/{taskId}:cancel  → cancelTask()
+ * POST   /{tenant}/tasks/{taskId}:subscribe → subscribeToTask()
+ * GET    /.well-known/agent-card.json     → getAgentCard()
+ * GET    /{tenant}/extendedAgentCard      → getExtendedAgentCard()
+ * 
+ * + *

Authentication

+ *

Most endpoints require authentication via {@code @Authenticated}, except: + *

+ * + *

Streaming Support

+ *

Streaming endpoints ({@code message:stream}, {@code subscribe}) use Server-Sent Events (SSE) + * via the inner {@link MultiSseSupport} class. SSE responses are handled by: + *

+ * + *

Error Handling

+ *

All errors are caught and converted to HTTP responses via {@link RestHandler#createErrorResponse(A2AError)}, + * ensuring consistent error format and status codes across all endpoints. + * + *

Context Creation

+ *

Each request creates a {@link ServerCallContext} via {@link #createCallContext(RoutingContext, String)}, + * extracting: + *

+ * + *

Custom context creation is supported via CDI-provided {@link CallContextFactory}. + * + * @see RestHandler + * @see ServerCallContext + * @see CallContextFactory + */ @Singleton @Authenticated public class A2AServerRoutes { @@ -87,6 +142,29 @@ public class A2AServerRoutes { @Inject Instance callContextFactory; + /** + * Handles blocking message send requests. + * + *

Maps {@code POST /{tenant}/message:send} to {@link RestHandler#sendMessage}. + * The request body must be JSON containing a message with parts. + * + *

URL Pattern: {@code /message:send} or {@code /{tenant}/message:send} + * + *

Example: + *

{@code
+     * POST /message:send
+     * Content-Type: application/json
+     *
+     * {
+     *   "message": {
+     *     "parts": [{"text": "Hello"}]
+     *   }
+     * }
+     * }
+ * + * @param body the JSON request body + * @param rc the Vert.x routing context + */ @Route(regex = "^\\/(?[^\\/]*\\/?)message:send$", order = 1, methods = {Route.HttpMethod.POST}, consumes = {APPLICATION_JSON}, type = Route.HandlerType.BLOCKING) public void sendMessage(@Body String body, RoutingContext rc) { ServerCallContext context = createCallContext(rc, SEND_MESSAGE_METHOD); @@ -100,6 +178,26 @@ public void sendMessage(@Body String body, RoutingContext rc) { } } + /** + * Handles streaming message send requests with Server-Sent Events. + * + *

Maps {@code POST /{tenant}/message:stream} to {@link RestHandler#sendStreamingMessage}. + * Returns a stream of task updates and artifacts as SSE events. + * + *

URL Pattern: {@code /message:stream} or {@code /{tenant}/message:stream} + * + *

Response Format: {@code text/event-stream} with JSON events: + *

{@code
+     * data: {"taskStatusUpdate":{"task":{"status":{"state":"WORKING"}}}}
+     *
+     * data: {"taskArtifactUpdate":{"artifacts":[...]}}
+     *
+     * data: {"taskStatusUpdate":{"task":{"status":{"state":"COMPLETED"}}}}
+     * }
+ * + * @param body the JSON request body + * @param rc the Vert.x routing context + */ @Route(regex = "^\\/(?[^\\/]*\\/?)message:stream$", order = 1, methods = {Route.HttpMethod.POST}, consumes = {APPLICATION_JSON}, type = Route.HandlerType.BLOCKING) public void sendMessageStreaming(@Body String body, RoutingContext rc) { ServerCallContext context = createCallContext(rc, SEND_STREAMING_MESSAGE_METHOD); @@ -129,6 +227,27 @@ public void sendMessageStreaming(@Body String body, RoutingContext rc) { } } + /** + * Lists tasks with optional filtering and pagination. + * + *

Maps {@code GET /{tenant}/tasks} to {@link RestHandler#listTasks}. + * Supports query parameters for filtering and pagination. + * + *

URL Pattern: {@code /tasks?status=COMPLETED&pageSize=10} + * + *

Query Parameters: + *

+ * + * @param rc the Vert.x routing context + */ @Route(regex = "^\\/(?[^\\/]*\\/?)tasks\\??", order = 0, methods = {Route.HttpMethod.GET}, type = Route.HandlerType.BLOCKING) public void listTasks(RoutingContext rc) { ServerCallContext context = createCallContext(rc, LIST_TASK_METHOD); @@ -174,6 +293,16 @@ public void listTasks(RoutingContext rc) { } } + /** + * Retrieves a specific task by ID. + * + *

Maps {@code GET /{tenant}/tasks/{taskId}} to {@link RestHandler#getTask}. + * Optionally includes task history via query parameter. + * + *

URL Pattern: {@code /tasks/{taskId}?historyLength=10} + * + * @param rc the Vert.x routing context (taskId extracted from path) + */ @Route(regex = "^\\/(?[^\\/]*\\/?)tasks\\/(?[^:^/]+)$", order = 1, methods = {Route.HttpMethod.GET}, type = Route.HandlerType.BLOCKING) public void getTask(RoutingContext rc) { String taskId = rc.pathParam("taskId"); @@ -198,6 +327,16 @@ public void getTask(RoutingContext rc) { } } + /** + * Cancels a running task. + * + *

Maps {@code POST /{tenant}/tasks/{taskId}:cancel} to {@link RestHandler#cancelTask}. + * Signals the agent executor to stop processing and transition to CANCELED state. + * + *

URL Pattern: {@code /tasks/{taskId}:cancel} + * + * @param rc the Vert.x routing context (taskId extracted from path) + */ @Route(regex = "^\\/(?[^\\/]*\\/?)tasks\\/(?[^/]+):cancel$", order = 1, methods = {Route.HttpMethod.POST}, type = Route.HandlerType.BLOCKING) public void cancelTask(RoutingContext rc) { String taskId = rc.pathParam("taskId"); @@ -220,6 +359,15 @@ public void cancelTask(RoutingContext rc) { } } + /** + * Sends an HTTP response from a {@link HTTPRestResponse} object. + * + *

Helper method that sets status code, content type header, and body + * from the response object. Used by all blocking endpoints. + * + * @param rc the Vert.x routing context + * @param response the response to send, or null to end without body + */ private void sendResponse(RoutingContext rc, @Nullable HTTPRestResponse response) { if (response != null) { rc.response() @@ -231,6 +379,23 @@ private void sendResponse(RoutingContext rc, @Nullable HTTPRestResponse response } } + /** + * Subscribes to task updates via Server-Sent Events. + * + *

Maps {@code POST /{tenant}/tasks/{taskId}:subscribe} to {@link RestHandler#subscribeToTask}. + * Returns a stream of task events allowing clients to reconnect to ongoing tasks. + * + *

URL Pattern: {@code /tasks/{taskId}:subscribe} + * + *

Use Cases: + *

    + *
  • Reconnecting after network interruption
  • + *
  • Monitoring long-running tasks
  • + *
  • Multiple clients observing same task
  • + *
+ * + * @param rc the Vert.x routing context (taskId extracted from path) + */ @Route(regex = "^\\/(?[^\\/]*\\/?)tasks\\/(?[^/]+):subscribe$", order = 1, methods = {Route.HttpMethod.POST}, type = Route.HandlerType.BLOCKING) public void subscribeToTask(RoutingContext rc) { String taskId = rc.pathParam("taskId"); @@ -265,6 +430,19 @@ public void subscribeToTask(RoutingContext rc) { } } + /** + * Creates a push notification configuration for a task. + * + *

Maps {@code POST /{tenant}/tasks/{taskId}/pushNotificationConfigs} to + * {@link RestHandler#createTaskPushNotificationConfiguration}. + * + *

URL Pattern: {@code /tasks/{taskId}/pushNotificationConfigs} + * + *

Request Body: JSON containing webhook URL and event filters + * + * @param body the JSON request body with notification configuration + * @param rc the Vert.x routing context (taskId extracted from path) + */ @Route(regex = "^\\/(?[^\\/]*\\/?)tasks\\/(?[^/]+)\\/pushNotificationConfigs$", order = 1, methods = {Route.HttpMethod.POST}, consumes = {APPLICATION_JSON}, type = Route.HandlerType.BLOCKING) public void CreateTaskPushNotificationConfiguration(@Body String body, RoutingContext rc) { String taskId = rc.pathParam("taskId"); @@ -283,6 +461,16 @@ public void CreateTaskPushNotificationConfiguration(@Body String body, RoutingCo } } + /** + * Retrieves a specific push notification configuration. + * + *

Maps {@code GET /{tenant}/tasks/{taskId}/pushNotificationConfigs/{configId}} to + * {@link RestHandler#getTaskPushNotificationConfiguration}. + * + *

URL Pattern: {@code /tasks/{taskId}/pushNotificationConfigs/{configId}} + * + * @param rc the Vert.x routing context (taskId and configId extracted from path) + */ @Route(regex = "^\\/(?[^\\/]*\\/?)tasks\\/(?[^/]+)\\/pushNotificationConfigs\\/(?[^\\/]+)", order = 2, methods = {Route.HttpMethod.GET}, type = Route.HandlerType.BLOCKING) public void getTaskPushNotificationConfiguration(RoutingContext rc) { String taskId = rc.pathParam("taskId"); @@ -304,6 +492,23 @@ public void getTaskPushNotificationConfiguration(RoutingContext rc) { } } + /** + * Lists push notification configurations for a task. + * + *

Maps {@code GET /{tenant}/tasks/{taskId}/pushNotificationConfigs} to + * {@link RestHandler#listTaskPushNotificationConfigurations}. + * Supports pagination via query parameters. + * + *

URL Pattern: {@code /tasks/{taskId}/pushNotificationConfigs?pageSize=10} + * + *

Query Parameters: + *

    + *
  • {@code pageSize} - Maximum configurations to return
  • + *
  • {@code pageToken} - Pagination token for next page
  • + *
+ * + * @param rc the Vert.x routing context (taskId extracted from path) + */ @Route(regex = "^\\/(?[^\\/]*\\/?)tasks\\/(?[^/]+)\\/pushNotificationConfigs\\/?$", order = 3, methods = {Route.HttpMethod.GET}, type = Route.HandlerType.BLOCKING) public void listTaskPushNotificationConfigurations(RoutingContext rc) { String taskId = rc.pathParam("taskId"); @@ -332,6 +537,18 @@ public void listTaskPushNotificationConfigurations(RoutingContext rc) { } } + /** + * Deletes a push notification configuration. + * + *

Maps {@code DELETE /{tenant}/tasks/{taskId}/pushNotificationConfigs/{configId}} to + * {@link RestHandler#deleteTaskPushNotificationConfiguration}. + * + *

URL Pattern: {@code /tasks/{taskId}/pushNotificationConfigs/{configId}} + * + *

Response: HTTP 204 No Content on success + * + * @param rc the Vert.x routing context (taskId and configId extracted from path) + */ @Route(regex = "^\\/(?[^\\/]*\\/?)tasks\\/(?[^/]+)\\/pushNotificationConfigs\\/(?[^/]+)", order = 1, methods = {Route.HttpMethod.DELETE}, type = Route.HandlerType.BLOCKING) public void deleteTaskPushNotificationConfiguration(RoutingContext rc) { String taskId = rc.pathParam("taskId"); @@ -353,6 +570,19 @@ public void deleteTaskPushNotificationConfiguration(RoutingContext rc) { } } + /** + * Extracts tenant ID from the routing context path parameter. + * + *

Handles optional tenant prefixes in URL paths, normalizing: + *

    + *
  • {@code /message:send} → empty string (default tenant)
  • + *
  • {@code /tenant-123/message:send} → "tenant-123"
  • + *
  • {@code /tenant-123} → "tenant-123" (strips leading/trailing slashes)
  • + *
+ * + * @param rc the Vert.x routing context + * @return the extracted tenant ID, or empty string if not specified + */ private String extractTenant(RoutingContext rc) { String tenantPath = rc.pathParam("tenant"); if (tenantPath == null || tenantPath.isBlank()) { @@ -368,11 +598,25 @@ private String extractTenant(RoutingContext rc) { } /** - * /** - * Handles incoming GET requests to the agent card endpoint. - * Returns the agent card in JSON format. + * Retrieves the public agent card for agent discovery. + * + *

Maps {@code GET /.well-known/agent-card.json} to {@link RestHandler#getAgentCard}. + * This is the primary discovery endpoint that clients use to understand agent capabilities, + * supported skills, and communication methods. * - * @param rc the routing context + *

URL Pattern: {@code /.well-known/agent-card.json} (well-known URI) + * + *

Authentication: {@code @PermitAll} - Public endpoint requiring no authentication + * + *

Response: JSON containing {@link io.a2a.spec.AgentCard} with: + *

    + *
  • Agent name, description, version
  • + *
  • Capabilities (streaming, push notifications)
  • + *
  • Supported skills
  • + *
  • Communication interfaces and protocols
  • + *
+ * + * @param rc the Vert.x routing context */ @Route(path = "/.well-known/agent-card.json", order = 1, methods = Route.HttpMethod.GET, produces = APPLICATION_JSON) @PermitAll @@ -381,12 +625,35 @@ public void getAgentCard(RoutingContext rc) { sendResponse(rc, response); } + /** + * Retrieves the extended agent card with additional metadata. + * + *

Maps {@code GET /{tenant}/extendedAgentCard} to {@link RestHandler#getExtendedAgentCard}. + * Provides tenant-specific or private capabilities beyond the public agent card. + * + *

URL Pattern: {@code /extendedAgentCard} or {@code /{tenant}/extendedAgentCard} + * + *

Authentication: Required (inherits {@code @Authenticated} from class) + * + * @param rc the Vert.x routing context + */ @Route(regex = "^\\/(?[^\\/]*\\/?)extendedAgentCard$", order = 1, methods = Route.HttpMethod.GET, produces = APPLICATION_JSON) public void getExtendedAgentCard(RoutingContext rc) { HTTPRestResponse response = jsonRestHandler.getExtendedAgentCard(createCallContext(rc, GET_EXTENDED_AGENT_CARD_METHOD), extractTenant(rc)); sendResponse(rc, response); } + /** + * Catch-all route for undefined endpoints. + * + *

Handles all HTTP methods on unmatched paths with order=100 (lowest priority). + * Returns a {@link io.a2a.spec.MethodNotFoundError} with HTTP 404 status. + * + *

Purpose: Provides consistent error responses for invalid API calls + * instead of generic 404 HTML pages. + * + * @param rc the Vert.x routing context + */ @Route(path = "^/.*", order = 100, methods = {Route.HttpMethod.DELETE, Route.HttpMethod.GET, Route.HttpMethod.HEAD, Route.HttpMethod.OPTIONS, Route.HttpMethod.POST, Route.HttpMethod.PUT}, produces = APPLICATION_JSON) public void methodNotFoundMessage(RoutingContext rc) { HTTPRestResponse response = jsonRestHandler.createErrorResponse(new MethodNotFoundError()); @@ -397,6 +664,36 @@ static void setStreamingMultiSseSupportSubscribedRunnable(Runnable runnable) { streamingMultiSseSupportSubscribedRunnable = runnable; } + /** + * Creates a {@link ServerCallContext} from Vert.x routing context. + * + *

This method extracts authentication, headers, and protocol metadata from the + * HTTP request and builds a context object that flows through the request processing + * pipeline to the agent executor. + * + *

Context Contents

+ *

The created context includes: + *

    + *
  • User: From Quarkus Security (or {@link UnauthenticatedUser} if absent)
  • + *
  • Headers: All HTTP headers as a map
  • + *
  • Tenant: Extracted from URL path parameter
  • + *
  • Method Name: A2A method being invoked
  • + *
  • Transport: {@link TransportProtocol#HTTP_JSON}
  • + *
  • Protocol Version: From {@code X-A2A-Version} header
  • + *
  • Extensions: From {@code X-A2A-Extensions} header
  • + *
+ * + *

Custom Context Factory

+ *

If a CDI bean implementing {@link CallContextFactory} is provided, it will be + * used instead of the default implementation. This allows applications to add custom + * context data or modify the extraction logic. + * + * @param rc the Vert.x routing context containing request data + * @param jsonRpcMethodName the A2A method name (e.g., "sendMessage", "cancelTask") + * @return a new ServerCallContext with extracted request metadata + * @see CallContextFactory + * @see ServerCallContext + */ private ServerCallContext createCallContext(RoutingContext rc, String jsonRpcMethodName) { if (callContextFactory.isUnsatisfied()) { User user; @@ -449,10 +746,45 @@ public String getUsername() { } /** - * Simplified SSE support for Vert.x/Quarkus. - *

- * This class only handles HTTP-specific concerns (writing to response, backpressure, disconnect). - * SSE formatting and JSON serialization are handled by {@link SseFormatter}. + * Server-Sent Events (SSE) streaming support for Vert.x/Quarkus. + * + *

This inner class handles the HTTP-specific aspects of SSE streaming: + *

    + *
  • Writing SSE-formatted events to the HTTP response
  • + *
  • Managing backpressure via {@link Flow.Subscription#request(long)}
  • + *
  • Detecting client disconnection and canceling upstream
  • + *
  • Setting appropriate SSE headers and chunked encoding
  • + *
+ * + *

SSE Format

+ *

Events are formatted by {@link SseFormatter} before being passed to this class. + * Each event follows the SSE specification: + *

+     * id: 0
+     * data: {"taskStatusUpdate":{...}}
+     *
+     * id: 1
+     * data: {"taskArtifactUpdate":{...}}
+     * 
+ * + *

Backpressure Handling

+ *

The subscriber requests one event at a time ({@code request(1)}) and only + * requests the next event after the previous write completes. This ensures the + * HTTP connection doesn't buffer excessive data if the client is slow. + * + *

Disconnect Detection

+ *

When the client closes the connection, this class: + *

    + *
  1. Calls {@link ServerCallContext#invokeEventConsumerCancelCallback()} to stop the event producer
  2. + *
  3. Cancels the upstream subscription to stop event generation
  4. + *
+ * + *

Write Queue Configuration

+ *

Critical: Sets {@code setWriteQueueMaxSize(1)} to force immediate flushing + * of each event. Without this, Vert.x buffers writes, causing delays in SSE delivery. + * + * @see SseFormatter + * @see ServerCallContext#invokeEventConsumerCancelCallback() */ private static class MultiSseSupport { private static final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(MultiSseSupport.class); @@ -462,11 +794,32 @@ private MultiSseSupport() { } /** - * Write SSE-formatted strings to HTTP response. + * Writes SSE-formatted event strings to the HTTP response with backpressure control. + * + *

This method subscribes to the event stream and writes each SSE-formatted string + * to the Vert.x HTTP response. It implements reactive backpressure by requesting + * events one at a time and only requesting the next after the previous write completes. + * + *

Execution Flow

+ *
    + *
  1. Subscribe to upstream {@code Multi} (SSE-formatted events)
  2. + *
  3. On first event: set SSE headers, disable buffering, write kickstart comment
  4. + *
  5. For each event: write to HTTP response asynchronously
  6. + *
  7. After write completes: request next event (backpressure control)
  8. + *
  9. On client disconnect or error: cancel upstream to stop event production
  10. + *
+ * + *

Headers Set

+ *
    + *
  • {@code Content-Type: text/event-stream}
  • + *
  • {@code Cache-Control: no-cache}
  • + *
  • {@code X-Accel-Buffering: no} (disable nginx buffering)
  • + *
  • Chunked encoding enabled
  • + *
* - * @param sseStrings Multi stream of SSE-formatted strings (from SseFormatter) - * @param rc Vert.x routing context - * @param context A2A server call context (for EventConsumer cancellation) + * @param sseStrings Multi stream of SSE-formatted strings (from {@link SseFormatter}) + * @param rc Vert.x routing context providing HTTP response + * @param context A2A server call context (for EventConsumer cancellation on disconnect) */ public static void writeSseStrings(Multi sseStrings, RoutingContext rc, ServerCallContext context) { HttpServerResponse response = rc.response(); diff --git a/reference/rest/src/main/java/io/a2a/server/rest/quarkus/CallContextFactory.java b/reference/rest/src/main/java/io/a2a/server/rest/quarkus/CallContextFactory.java index 7aa5caf5e..b6432d1b3 100644 --- a/reference/rest/src/main/java/io/a2a/server/rest/quarkus/CallContextFactory.java +++ b/reference/rest/src/main/java/io/a2a/server/rest/quarkus/CallContextFactory.java @@ -3,6 +3,61 @@ import io.a2a.server.ServerCallContext; import io.vertx.ext.web.RoutingContext; +/** + * Factory interface for creating {@link ServerCallContext} from Vert.x routing context. + * + *

This interface provides an extension point for customizing how {@link ServerCallContext} + * instances are created in Quarkus REST applications. The default implementation in + * {@link A2AServerRoutes} extracts standard information (user, headers, tenant, protocol version), + * but applications can provide their own implementation to add custom context data. + * + *

Default Behavior

+ *

When no CDI bean implementing this interface is provided, {@link A2AServerRoutes} + * creates contexts with: + *

    + *
  • User authentication from Quarkus Security
  • + *
  • HTTP headers map
  • + *
  • Tenant ID from URL path
  • + *
  • A2A protocol version from {@code X-A2A-Version} header
  • + *
  • Required extensions from {@code X-A2A-Extensions} header
  • + *
+ * + *

Custom Implementation Example

+ *
{@code
+ * @ApplicationScoped
+ * public class CustomCallContextFactory implements CallContextFactory {
+ *     @Override
+ *     public ServerCallContext build(RoutingContext rc) {
+ *         // Extract custom data from routing context
+ *         String orgId = rc.request().getHeader("X-Organization-ID");
+ *
+ *         Map state = new HashMap<>();
+ *         state.put("organization", orgId);
+ *         state.put("requestId", UUID.randomUUID().toString());
+ *
+ *         return new ServerCallContext(
+ *             extractUser(rc),
+ *             state,
+ *             extractExtensions(rc),
+ *             extractVersion(rc)
+ *         );
+ *     }
+ * }
+ * }
+ * + * @see ServerCallContext + * @see A2AServerRoutes#createCallContext(RoutingContext, String) + */ public interface CallContextFactory { + /** + * Builds a {@link ServerCallContext} from a Vert.x routing context. + * + *

This method is called for each incoming HTTP request to create the context + * that will be passed to the {@link io.a2a.server.requesthandlers.RequestHandler} + * and eventually to the {@link io.a2a.server.agentexecution.AgentExecutor}. + * + * @param rc the Vert.x routing context containing request data + * @return a new ServerCallContext with extracted authentication, headers, and metadata + */ ServerCallContext build(RoutingContext rc); } diff --git a/reference/rest/src/main/java/io/a2a/server/rest/quarkus/QuarkusRestTransportMetadata.java b/reference/rest/src/main/java/io/a2a/server/rest/quarkus/QuarkusRestTransportMetadata.java index ee9d3ae98..65ab017d7 100644 --- a/reference/rest/src/main/java/io/a2a/server/rest/quarkus/QuarkusRestTransportMetadata.java +++ b/reference/rest/src/main/java/io/a2a/server/rest/quarkus/QuarkusRestTransportMetadata.java @@ -3,7 +3,30 @@ import io.a2a.server.TransportMetadata; import io.a2a.spec.TransportProtocol; +/** + * Transport metadata implementation for Quarkus REST. + * + *

This class provides transport protocol identification for the Quarkus REST + * reference implementation. It reports {@link TransportProtocol#HTTP_JSON} as + * the transport protocol, indicating that this implementation uses HTTP with + * JSON payloads for the A2A protocol. + * + *

The transport metadata is used by the framework for: + *

    + *
  • Logging and monitoring (identifying which transport handled a request)
  • + *
  • Protocol negotiation and version compatibility checks
  • + *
  • Metrics and telemetry (transport-specific performance tracking)
  • + *
+ * + * @see TransportMetadata + * @see TransportProtocol + */ public class QuarkusRestTransportMetadata implements TransportMetadata { + /** + * Returns the transport protocol identifier. + * + * @return {@code "http+json"} indicating HTTP transport with JSON encoding + */ @Override public String getTransportProtocol() { return TransportProtocol.HTTP_JSON.asString(); diff --git a/reference/rest/src/main/java/io/a2a/server/rest/quarkus/package-info.java b/reference/rest/src/main/java/io/a2a/server/rest/quarkus/package-info.java index 00ca87bd6..2e76b40eb 100644 --- a/reference/rest/src/main/java/io/a2a/server/rest/quarkus/package-info.java +++ b/reference/rest/src/main/java/io/a2a/server/rest/quarkus/package-info.java @@ -1,3 +1,65 @@ +/** + * Quarkus REST reference implementation for the A2A protocol. + * + *

This package provides a ready-to-use Quarkus-based REST transport implementation + * that maps HTTP endpoints to A2A protocol operations. It serves as both a reference + * implementation and a production-ready solution for deploying A2A agents over HTTP/REST. + * + *

Key Components

+ *
    + *
  • {@link A2AServerRoutes} - Vert.x route definitions mapping HTTP paths to A2A operations
  • + *
  • {@link CallContextFactory} - Extensible factory for creating {@link io.a2a.server.ServerCallContext}
  • + *
  • {@link QuarkusRestTransportMetadata} - Transport protocol metadata implementation
  • + *
+ * + *

Architecture

+ *
+ * HTTP Request (Quarkus/Vert.x)
+ *     ↓
+ * A2AServerRoutes (@Route methods)
+ *     ↓
+ * RestHandler (transport/rest)
+ *     ↓
+ * RequestHandler (server-common)
+ *     ↓
+ * AgentExecutor (user implementation)
+ * 
+ * + *

Supported Endpoints

+ *
    + *
  • {@code POST /message:send} - Send message (blocking)
  • + *
  • {@code POST /message:stream} - Send message (streaming SSE)
  • + *
  • {@code GET /tasks} - List tasks
  • + *
  • {@code GET /tasks/{taskId}} - Get task by ID
  • + *
  • {@code POST /tasks/{taskId}:cancel} - Cancel task
  • + *
  • {@code POST /tasks/{taskId}:subscribe} - Subscribe to task updates (SSE)
  • + *
  • {@code GET /.well-known/agent-card.json} - Get agent card
  • + *
+ * + *

Multi-tenancy Support

+ *

All endpoints support optional tenant prefixes in the URL path: + *

    + *
  • {@code /message:send} - Default tenant (empty string)
  • + *
  • {@code /tenant-123/message:send} - Tenant "tenant-123"
  • + *
+ * + *

Usage

+ *

Add this module as a dependency to your Quarkus project: + *

{@code
+ * 
+ *   io.github.a2asdk
+ *   a2a-java-sdk-reference-rest
+ *   ${a2a.version}
+ * 
+ * }
+ * + *

Provide CDI beans for {@link io.a2a.spec.AgentCard} and + * {@link io.a2a.server.agentexecution.AgentExecutor}, and the REST endpoints + * will be automatically registered. + * + * @see io.a2a.transport.rest.handler + * @see io.a2a.server.requesthandlers + */ @NullMarked package io.a2a.server.rest.quarkus; diff --git a/transport/rest/src/main/java/io/a2a/transport/rest/context/RestContextKeys.java b/transport/rest/src/main/java/io/a2a/transport/rest/context/RestContextKeys.java index f822607ef..86e78e1ad 100644 --- a/transport/rest/src/main/java/io/a2a/transport/rest/context/RestContextKeys.java +++ b/transport/rest/src/main/java/io/a2a/transport/rest/context/RestContextKeys.java @@ -3,8 +3,20 @@ /** * Shared REST context keys for A2A protocol data. * - * These keys provide access to REST context information, - * enabling rich context access in service method implementations. + *

These keys provide access to REST context information stored in + * {@link io.a2a.server.ServerCallContext}, enabling rich context access + * in service method implementations and middleware. + * + *

Usage Example

+ *
{@code
+ * public void processRequest(ServerCallContext context) {
+ *     String tenant = context.get(RestContextKeys.TENANT_KEY);
+ *     String method = context.get(RestContextKeys.METHOD_NAME_KEY);
+ *     Map headers = context.get(RestContextKeys.HEADERS_KEY);
+ * }
+ * }
+ * + * @see io.a2a.server.ServerCallContext */ public final class RestContextKeys { diff --git a/transport/rest/src/main/java/io/a2a/transport/rest/handler/RestHandler.java b/transport/rest/src/main/java/io/a2a/transport/rest/handler/RestHandler.java index 7312824c2..e743c4cc3 100644 --- a/transport/rest/src/main/java/io/a2a/transport/rest/handler/RestHandler.java +++ b/transport/rest/src/main/java/io/a2a/transport/rest/handler/RestHandler.java @@ -65,6 +65,49 @@ import mutiny.zero.ZeroPublisher; import org.jspecify.annotations.Nullable; +/** + * REST transport handler for processing A2A protocol requests over HTTP. + * + *

This handler converts HTTP REST requests into A2A protocol operations and + * manages the lifecycle of agent interactions including message sending, task + * management, and push notification configurations. + * + *

Request Flow

+ *

HTTP REST requests flow through this handler to the underlying {@link RequestHandler}, + * which coordinates with the agent executor and event queue system: + *

+ * HTTP Request → RestHandler → RequestHandler → AgentExecutor
+ *                    ↓              ↓
+ *              Validation    EventQueue → Response
+ * 
+ * + *

Supported Operations

+ *
    + *
  • Message sending (blocking and streaming)
  • + *
  • Task management (get, list, cancel, subscribe)
  • + *
  • Push notification configurations (create, get, list, delete)
  • + *
  • Agent card retrieval (public and extended)
  • + *
+ * + *

Error Handling

+ *

All A2A protocol errors are caught and converted to appropriate HTTP status codes + * via {@link #mapErrorToHttpStatus(A2AError)}. Protocol version and required extensions + * are validated before processing requests. + * + *

CDI Integration

+ *

This handler is an {@code @ApplicationScoped} CDI bean that requires: + *

    + *
  • {@link AgentCard} qualified with {@code @PublicAgentCard}
  • + *
  • {@link RequestHandler} for processing A2A operations
  • + *
  • {@link Executor} qualified with {@code @Internal} for async operations
  • + *
  • Optional {@link AgentCard} qualified with {@code @ExtendedAgentCard}
  • + *
+ * + * @see RequestHandler + * @see io.a2a.server.requesthandlers.DefaultRequestHandler + * @see io.a2a.spec.AgentCard + * @see ServerCallContext + */ @ApplicationScoped public class RestHandler { @@ -90,6 +133,14 @@ protected RestHandler() { this.executor = null; } + /** + * Creates a REST handler with full CDI injection support. + * + * @param agentCard the public agent card containing agent capabilities + * @param extendedAgentCard optional extended agent card instance + * @param requestHandler the handler for processing A2A requests + * @param executor the executor for asynchronous operations + */ @Inject public RestHandler(@PublicAgentCard AgentCard agentCard, @ExtendedAgentCard Instance extendedAgentCard, RequestHandler requestHandler, @Internal Executor executor) { @@ -102,12 +153,62 @@ public RestHandler(@PublicAgentCard AgentCard agentCard, @ExtendedAgentCard Inst AgentCardValidator.validateTransportConfiguration(agentCard); } + /** + * Creates a REST handler with basic dependencies. + * + * @param agentCard the agent card containing agent capabilities + * @param requestHandler the handler for processing A2A requests + * @param executor the executor for asynchronous operations + */ public RestHandler(AgentCard agentCard, RequestHandler requestHandler, Executor executor) { this.agentCard = agentCard; this.requestHandler = requestHandler; this.executor = executor; } + /** + * Handles a blocking message send request. + * + *

This method processes an HTTP POST request containing a message to be sent to the agent. + * The request is validated for protocol version and required extensions before being forwarded + * to the {@link RequestHandler}. The method blocks until the agent produces a terminal event + * or requires authentication/input. + * + *

Example Request:

+ *
{@code
+     * POST /v1/tenants/{tenant}/messages
+     * Content-Type: application/json
+     *
+     * {
+     *   "message": {
+     *     "parts": [
+     *       {"text": "What is the weather in San Francisco?"}
+     *     ]
+     *   }
+     * }
+     * }
+ * + *

Example Response:

+ *
{@code
+     * HTTP/1.1 200 OK
+     * Content-Type: application/json
+     *
+     * {
+     *   "task": {
+     *     "id": "task-123",
+     *     "status": {"state": "COMPLETED"},
+     *     "artifacts": [...]
+     *   }
+     * }
+     * }
+ * + * @param context the server call context containing authentication and metadata + * @param tenant the tenant identifier + * @param body the JSON request body containing the message to send + * @return the HTTP response containing the task or message result + * @see #sendStreamingMessage(ServerCallContext, String, String) + * @see RequestHandler#onMessageSend(io.a2a.spec.MessageSendParams, ServerCallContext) + */ public HTTPRestResponse sendMessage(ServerCallContext context, String tenant, String body) { try { @@ -125,6 +226,51 @@ public HTTPRestResponse sendMessage(ServerCallContext context, String tenant, St } } + /** + * Handles a streaming message send request. + * + *

This method processes an HTTP POST request for streaming responses from the agent. + * The response is returned as Server-Sent Events (SSE) via {@link HTTPRestStreamingResponse}, + * allowing clients to receive task updates and artifacts as they are produced by the agent. + * + *

This method requires the agent card to have {@code capabilities.streaming = true}. + * + *

Example Request:

+ *
{@code
+     * POST /v1/tenants/{tenant}/messages/stream
+     * Content-Type: application/json
+     *
+     * {
+     *   "message": {
+     *     "parts": [
+     *       {"text": "Generate a long story"}
+     *     ]
+     *   }
+     * }
+     * }
+ * + *

Example Streaming Response:

+ *
{@code
+     * HTTP/1.1 200 OK
+     * Content-Type: text/event-stream
+     *
+     * data: {"taskStatusUpdate":{"task":{"id":"task-123","status":{"state":"WORKING"}}}}
+     *
+     * data: {"taskArtifactUpdate":{"taskId":"task-123","artifacts":[{"parts":[{"text":"Once upon"}]}]}}
+     *
+     * data: {"taskArtifactUpdate":{"taskId":"task-123","artifacts":[{"parts":[{"text":" a time..."}]}]}}
+     *
+     * data: {"taskStatusUpdate":{"task":{"id":"task-123","status":{"state":"COMPLETED"}}}}
+     * }
+ * + * @param context the server call context containing authentication and metadata + * @param tenant the tenant identifier + * @param body the JSON request body containing the message to send + * @return the streaming HTTP response containing a publisher of events + * @see #sendMessage(ServerCallContext, String, String) + * @see RequestHandler#onMessageSendStream(io.a2a.spec.MessageSendParams, ServerCallContext) + * @see HTTPRestStreamingResponse + */ public HTTPRestResponse sendStreamingMessage(ServerCallContext context, String tenant, String body) { try { if (!agentCard.capabilities().streaming()) { @@ -144,6 +290,26 @@ public HTTPRestResponse sendStreamingMessage(ServerCallContext context, String t } } + /** + * Handles a task cancellation request. + * + *

Attempts to cancel a running task identified by the task ID. The cancellation + * request is forwarded to the {@link RequestHandler}, which signals the agent executor + * to stop processing. The agent should transition the task to {@code CANCELED} state. + * + *

Example Request:

+ *
{@code
+     * POST /v1/tenants/{tenant}/tasks/{taskId}/cancel
+     * }
+ * + * @param context the server call context containing authentication and metadata + * @param tenant the tenant identifier + * @param taskId the ID of the task to cancel + * @return the HTTP response containing the cancelled task + * @throws InvalidParamsError if taskId is null or empty + * @see RequestHandler#onCancelTask(TaskIdParams, ServerCallContext) + * @see io.a2a.server.agentexecution.AgentExecutor#cancel + */ public HTTPRestResponse cancelTask(ServerCallContext context, String tenant, String taskId) { try { if (taskId == null || taskId.isEmpty()) { @@ -162,6 +328,15 @@ public HTTPRestResponse cancelTask(ServerCallContext context, String tenant, Str } } + /** + * Creates a push notification configuration for a task. + * + * @param context the server call context containing authentication and metadata + * @param tenant the tenant identifier + * @param body the JSON request body containing the configuration + * @param taskId the ID of the task + * @return the HTTP response containing the created configuration + */ public HTTPRestResponse createTaskPushNotificationConfiguration(ServerCallContext context, String tenant, String body, String taskId) { try { if (!agentCard.capabilities().pushNotifications()) { @@ -179,6 +354,34 @@ public HTTPRestResponse createTaskPushNotificationConfiguration(ServerCallContex } } + /** + * Subscribes to task updates via a streaming connection. + * + *

Creates a Server-Sent Events (SSE) stream that delivers real-time updates for an + * existing task. This allows clients to reconnect to ongoing or completed tasks and + * receive their event history and future updates. + * + *

This method requires the agent card to have {@code capabilities.streaming = true}. + * + *

Example Request:

+ *
{@code
+     * GET /v1/tenants/{tenant}/tasks/{taskId}/subscribe
+     * }
+ * + *

Use Cases:

+ *
    + *
  • Reconnecting to a task after network interruption
  • + *
  • Monitoring long-running tasks from multiple clients
  • + *
  • Viewing historical events for completed tasks
  • + *
+ * + * @param context the server call context containing authentication and metadata + * @param tenant the tenant identifier + * @param taskId the ID of the task to subscribe to + * @return the streaming HTTP response containing task updates + * @see RequestHandler#onSubscribeToTask(TaskIdParams, ServerCallContext) + * @see #sendStreamingMessage(ServerCallContext, String, String) + */ public HTTPRestResponse subscribeToTask(ServerCallContext context, String tenant, String taskId) { try { if (!agentCard.capabilities().streaming()) { @@ -194,6 +397,15 @@ public HTTPRestResponse subscribeToTask(ServerCallContext context, String tenant } } + /** + * Retrieves a task by ID. + * + * @param context the server call context containing authentication and metadata + * @param tenant the tenant identifier + * @param taskId the ID of the task to retrieve + * @param historyLength the maximum number of history entries to include + * @return the HTTP response containing the task + */ public HTTPRestResponse getTask(ServerCallContext context, String tenant, String taskId, @Nullable Integer historyLength) { try { TaskQueryParams params = new TaskQueryParams(taskId, historyLength, tenant); @@ -209,6 +421,43 @@ public HTTPRestResponse getTask(ServerCallContext context, String tenant, String } } + /** + * Lists tasks with optional filtering and pagination. + * + *

Retrieves a list of tasks with support for filtering by context, status, and timestamp, + * along with pagination controls. This method is useful for task management dashboards, + * monitoring systems, and task history retrieval. + * + *

Example Request:

+ *
{@code
+     * GET /v1/tenants/{tenant}/tasks?status=COMPLETED&pageSize=10&includeArtifacts=true
+     * }
+ * + *

Query Parameters:

+ *
    + *
  • {@code contextId} - Filter tasks by conversation context
  • + *
  • {@code status} - Filter by task state (SUBMITTED, WORKING, COMPLETED, etc.)
  • + *
  • {@code pageSize} - Maximum tasks to return (for pagination)
  • + *
  • {@code pageToken} - Token for retrieving next page of results
  • + *
  • {@code historyLength} - Maximum history entries to include per task
  • + *
  • {@code statusTimestampAfter} - ISO-8601 timestamp for filtering recent tasks
  • + *
  • {@code includeArtifacts} - Whether to include task artifacts in response
  • + *
+ * + * @param context the server call context containing authentication and metadata + * @param tenant the tenant identifier + * @param contextId optional context ID to filter by + * @param status optional task status to filter by (must be valid {@link TaskState} value) + * @param pageSize optional maximum number of tasks to return + * @param pageToken optional token for pagination + * @param historyLength optional maximum number of history entries per task + * @param statusTimestampAfter optional ISO-8601 timestamp to filter tasks updated after + * @param includeArtifacts optional flag to include task artifacts + * @return the HTTP response containing the list of tasks + * @throws InvalidParamsError if status is not a valid TaskState or timestamp is malformed + * @see RequestHandler#onListTasks(ListTasksParams, ServerCallContext) + * @see TaskState + */ public HTTPRestResponse listTasks(ServerCallContext context, String tenant, @Nullable String contextId, @Nullable String status, @Nullable Integer pageSize, @Nullable String pageToken, @@ -271,6 +520,15 @@ public HTTPRestResponse listTasks(ServerCallContext context, String tenant, } } + /** + * Retrieves a specific push notification configuration for a task. + * + * @param context the server call context containing authentication and metadata + * @param tenant the tenant identifier + * @param taskId the ID of the task + * @param configId the ID of the configuration to retrieve + * @return the HTTP response containing the configuration + */ public HTTPRestResponse getTaskPushNotificationConfiguration(ServerCallContext context, String tenant, String taskId, String configId) { try { if (!agentCard.capabilities().pushNotifications()) { @@ -286,6 +544,16 @@ public HTTPRestResponse getTaskPushNotificationConfiguration(ServerCallContext c } } + /** + * Lists push notification configurations for a task. + * + * @param context the server call context containing authentication and metadata + * @param tenant the tenant identifier + * @param taskId the ID of the task + * @param pageSize the maximum number of configurations to return + * @param pageToken the token for pagination + * @return the HTTP response containing the list of configurations + */ public HTTPRestResponse listTaskPushNotificationConfigurations(ServerCallContext context, String tenant, String taskId, int pageSize, String pageToken) { try { if (!agentCard.capabilities().pushNotifications()) { @@ -301,6 +569,15 @@ public HTTPRestResponse listTaskPushNotificationConfigurations(ServerCallContext } } + /** + * Deletes a push notification configuration for a task. + * + * @param context the server call context containing authentication and metadata + * @param tenant the tenant identifier + * @param taskId the ID of the task + * @param configId the ID of the configuration to delete + * @return the HTTP response with no content on success + */ public HTTPRestResponse deleteTaskPushNotificationConfiguration(ServerCallContext context, String tenant, String taskId, String configId) { try { if (!agentCard.capabilities().pushNotifications()) { @@ -348,6 +625,12 @@ private HTTPRestResponse createSuccessResponse(int statusCode, com.google.protob } } + /** + * Creates an HTTP error response from an A2A error. + * + * @param error the A2A error to convert + * @return the HTTP response with appropriate status code and error details + */ public HTTPRestResponse createErrorResponse(A2AError error) { int statusCode = mapErrorToHttpStatus(error); return createErrorResponse(statusCode, error); @@ -425,6 +708,24 @@ public void onComplete() { }); } + /** + * Maps A2A protocol errors to HTTP status codes. + * + *

This method ensures consistent HTTP status code mapping for all A2A errors: + *

    + *
  • 400 - Invalid request, JSON parse errors, missing extensions
  • + *
  • 404 - Method not found, task not found
  • + *
  • 409 - Task not cancelable (conflict)
  • + *
  • 415 - Unsupported content type
  • + *
  • 422 - Invalid parameters (unprocessable entity)
  • + *
  • 500 - Internal errors
  • + *
  • 501 - Not implemented (unsupported operations, version)
  • + *
  • 502 - Bad gateway (invalid agent response)
  • + *
+ * + * @param error the A2A error to map + * @return the corresponding HTTP status code + */ private int mapErrorToHttpStatus(A2AError error) { if (error instanceof InvalidRequestError || error instanceof JSONParseError) { return 400; @@ -459,6 +760,26 @@ private int mapErrorToHttpStatus(A2AError error) { return 500; } + /** + * Retrieves the extended agent card if configured. + * + *

The extended agent card provides additional metadata beyond the public agent card, + * such as tenant-specific configurations or private capabilities. This endpoint requires + * the agent card to have {@code capabilities.extendedAgentCard = true} and a CDI-produced + * {@code @ExtendedAgentCard} instance. + * + *

Example Request:

+ *
{@code
+     * GET /v1/tenants/{tenant}/extended-agent-card
+     * }
+ * + * @param context the server call context containing authentication and metadata + * @param tenant the tenant identifier + * @return the HTTP response containing the extended agent card + * @throws ExtendedAgentCardNotConfiguredError if extended agent card is not available + * @see #getAgentCard() + * @see AgentCard + */ public HTTPRestResponse getExtendedAgentCard(ServerCallContext context, String tenant) { try { if (!agentCard.capabilities().extendedAgentCard() || extendedAgentCard == null || !extendedAgentCard.isResolvable()) { @@ -472,6 +793,38 @@ public HTTPRestResponse getExtendedAgentCard(ServerCallContext context, String t } } + /** + * Retrieves the public agent card. + * + *

The agent card is a self-describing manifest that provides essential metadata about + * the agent, including its capabilities, supported skills, communication methods, and + * security requirements. This is the primary discovery endpoint for clients to understand + * what the agent can do and how to interact with it. + * + *

Example Request:

+ *
{@code
+     * GET /v1/agent-card
+     * }
+ * + *

Example Response:

+ *
{@code
+     * {
+     *   "name": "Weather Agent",
+     *   "description": "Provides weather information",
+     *   "version": "1.0.0",
+     *   "capabilities": {
+     *     "streaming": true,
+     *     "pushNotifications": false
+     *   },
+     *   "skills": [...],
+     *   "supportedInterfaces": [...]
+     * }
+     * }
+ * + * @return the HTTP response containing the agent card + * @see AgentCard + * @see #getExtendedAgentCard(ServerCallContext, String) + */ public HTTPRestResponse getAgentCard() { try { return new HTTPRestResponse(200, "application/json", JsonUtil.toJson(agentCard)); @@ -480,26 +833,51 @@ public HTTPRestResponse getAgentCard() { } } + /** + * Represents an HTTP REST response with status code, content type, and body. + */ public static class HTTPRestResponse { private final int statusCode; private final String contentType; private final String body; + /** + * Creates an HTTP REST response. + * + * @param statusCode the HTTP status code + * @param contentType the content type of the response + * @param body the response body + */ public HTTPRestResponse(int statusCode, String contentType, String body) { this.statusCode = statusCode; this.contentType = contentType; this.body = body; } + /** + * Returns the HTTP status code. + * + * @return the status code + */ public int getStatusCode() { return statusCode; } + /** + * Returns the content type. + * + * @return the content type + */ public String getContentType() { return contentType; } + /** + * Returns the response body. + * + * @return the body + */ public String getBody() { return body; } @@ -510,26 +888,47 @@ public String toString() { } } + /** + * Represents an HTTP streaming response with Server-Sent Events. + */ public static class HTTPRestStreamingResponse extends HTTPRestResponse { private final Flow.Publisher publisher; + /** + * Creates an HTTP streaming response. + * + * @param publisher the publisher of streaming events + */ public HTTPRestStreamingResponse(Flow.Publisher publisher) { super(200, "text/event-stream", ""); this.publisher = publisher; } + /** + * Returns the publisher for streaming events. + * + * @return the publisher + */ public Flow.Publisher getPublisher() { return publisher; } } + /** + * Represents an HTTP error response containing A2A error details. + */ private static class HTTPRestErrorResponse { private final String error; private final @Nullable String message; + /** + * Creates an error response from an A2A error. + * + * @param jsonRpcError the A2A error + */ private HTTPRestErrorResponse(A2AError jsonRpcError) { this.error = jsonRpcError.getClass().getName(); this.message = jsonRpcError.getMessage(); diff --git a/transport/rest/src/main/java/io/a2a/transport/rest/handler/package-info.java b/transport/rest/src/main/java/io/a2a/transport/rest/handler/package-info.java index 8d4e4063c..d8ea46ac7 100644 --- a/transport/rest/src/main/java/io/a2a/transport/rest/handler/package-info.java +++ b/transport/rest/src/main/java/io/a2a/transport/rest/handler/package-info.java @@ -1,3 +1,15 @@ +/** + * REST transport handler implementations for the A2A protocol. + * + *

This package contains the core REST handler that processes HTTP requests + * and translates them to A2A protocol operations. It includes support for: + *

    + *
  • Message sending (blocking and streaming)
  • + *
  • Task management and querying
  • + *
  • Push notification configurations
  • + *
  • Agent card retrieval
  • + *
+ */ @NullMarked package io.a2a.transport.rest.handler; diff --git a/transport/rest/src/test/java/io/a2a/transport/rest/handler/RestTestTransportMetadata.java b/transport/rest/src/test/java/io/a2a/transport/rest/handler/RestTestTransportMetadata.java index 68aad41bb..d9401d68e 100644 --- a/transport/rest/src/test/java/io/a2a/transport/rest/handler/RestTestTransportMetadata.java +++ b/transport/rest/src/test/java/io/a2a/transport/rest/handler/RestTestTransportMetadata.java @@ -3,7 +3,16 @@ import io.a2a.server.TransportMetadata; import io.a2a.spec.TransportProtocol; +/** + * Test implementation of TransportMetadata for REST transport testing. + */ public class RestTestTransportMetadata implements TransportMetadata { + + /** + * Returns the transport protocol used for REST communication. + * + * @return the HTTP JSON transport protocol identifier + */ @Override public String getTransportProtocol() { return TransportProtocol.HTTP_JSON.asString();