From 9ac21fa48a672f27a7b5ea707c24a1631a6bc3ea Mon Sep 17 00:00:00 2001 From: Emmanuel Hugonnet Date: Wed, 1 Jul 2026 13:17:58 +0200 Subject: [PATCH] docs: move inline documentation from README to project website Replace ~870 lines of detailed setup, configuration, and API usage documentation in README.md with a slim overview linking to a2aproject.github.io/a2a-java. Migrate the removed content into docs/content/server.md (configuration system, task authorization, user identity, v0.3 backward compatibility) and docs/content/client.md (push notification config, REST transport, v0.3 client notes). Signed-off-by: Emmanuel Hugonnet --- README.md | 897 ++--------------------------------------- docs/content/client.md | 21 + docs/content/server.md | 101 ++++- 3 files changed, 148 insertions(+), 871 deletions(-) diff --git a/README.md b/README.md index 7ef5915e0..6f5d36f29 100644 --- a/README.md +++ b/README.md @@ -8,55 +8,26 @@

A2A Logo

-

A Java library that helps run agentic applications as A2AServers following the Agent2Agent (A2A) Protocol.

+

A Java library that helps run agentic applications as A2A servers following the Agent2Agent (A2A) Protocol.

-## Installation +A multi-module Maven library providing client and server support for A2A agent communication over JSON-RPC, gRPC, and REST transports. -You can build the A2A Java SDK using `mvn`: +## Documentation -```bash -mvn clean install -``` - -### Regeneration of gRPC files -We copy https://github.com/a2aproject/A2A/blob/main/specification/grpc/a2a.proto to the [`spec-grpc/`](./spec-grpc) project, and adjust the `java_package` option to be as follows: -``` -option java_package = "org.a2aproject.sdk.grpc"; -``` -Then build the `spec-grpc` module with `mvn clean install -Dskip.protobuf.generate=false` to regenerate the gRPC classes in the `org.a2aproject.sdk.grpc` package. - -## Examples - -You can find examples of how to use the A2A Java SDK in the [a2a-samples repository](https://github.com/a2aproject/a2a-samples/tree/main/samples/java/agents). - -More examples will be added soon. - -## A2A Server - -The A2A Java SDK provides a Java server implementation of the [Agent2Agent (A2A) Protocol](https://a2a-protocol.org/). To run your agentic Java application as an A2A server, simply follow the steps below. +Full documentation is available at **[a2aproject.github.io/a2a-java](https://a2aproject.github.io/a2a-java/)**. -- [Add an A2A Java SDK Server Maven dependency to your project](#1-add-an-a2a-java-sdk-server-maven-dependency-to-your-project) -- [Add a class that creates an A2A Agent Card](#2-add-a-class-that-creates-an-a2a-agent-card) -- [Add a class that creates an A2A Agent Executor](#3-add-a-class-that-creates-an-a2a-agent-executor) +- [Server Guide](https://a2aproject.github.io/a2a-java/server) — Run your agentic Java application as an A2A server +- [Client Guide](https://a2aproject.github.io/a2a-java/client) — Communicate with any A2A-compliant agent +- [Community Articles](https://a2aproject.github.io/a2a-java/community) — Tutorials, blog posts, and videos +- [Announcements](https://a2aproject.github.io/a2a-java/announces) — Release announcements and project news +- [Contributing](https://a2aproject.github.io/a2a-java/contributing) — Developer guide for contributing -### 1. Add an A2A Java SDK Server Maven dependency to your project +## Quick Start -Adding a dependency on an A2A Java SDK Server will provide access to the core classes -that make up the A2A specification and allow you to run your agentic Java application as an A2A server agent. +Requires Java 17+. -The A2A Java SDK provides [reference A2A server implementations](reference) based on [Quarkus](https://quarkus.io) for use with our tests and examples. However, the project is designed in such a way that it is trivial to integrate with various Java runtimes. - -[Server Integrations](#server-integrations) contains a list of community contributed integrations of the server with various runtimes. You might be able to use one of these for your target runtime, or you can use them as inspiration to create your own. - -#### Server Transports -The A2A Java SDK Reference Server implementations support the following transports: - -* JSON-RPC 2.0 -* gRPC -* HTTP+JSON/REST - -To use the reference implementation with the JSON-RPC protocol, add the following dependency to your project: +Add the A2A Java SDK reference server for JSON-RPC to your Maven project: ```xml @@ -67,834 +38,40 @@ To use the reference implementation with the JSON-RPC protocol, add the followin ``` -To use the reference implementation with the gRPC protocol, add the following dependency to your project: - -```xml - - org.a2aproject.sdk - a2a-java-sdk-reference-grpc - - ${org.a2aproject.sdk.version} - -``` - -To use the reference implementation with the HTTP+JSON/REST protocol, add the following dependency to your project: - -```xml - - org.a2aproject.sdk - a2a-java-sdk-reference-rest - - ${org.a2aproject.sdk.version} - -``` - -Note that you can add more than one of the above dependencies to your project depending on the transports -you'd like to support. - -### 2. Add a class that creates an A2A Agent Card - -```java -import org.a2aproject.sdk.server.PublicAgentCard; -import org.a2aproject.sdk.spec.AgentCapabilities; -import org.a2aproject.sdk.spec.AgentCard; -import org.a2aproject.sdk.spec.AgentInterface; -import org.a2aproject.sdk.spec.AgentSkill; -import org.a2aproject.sdk.spec.TransportProtocol; -... - -@ApplicationScoped -public class WeatherAgentCardProducer { - - private static final String AGENT_URL = "http://localhost:10001"; - - @Produces - @PublicAgentCard - public AgentCard agentCard() { - return AgentCard.builder() - .name("Weather Agent") - .description("Helps with weather") - .supportedInterfaces(List.of( - new AgentInterface(TransportProtocol.JSONRPC.asString(), AGENT_URL))) - .version("1.0.0") - .capabilities(AgentCapabilities.builder() - .streaming(true) - .pushNotifications(false) - .build()) - .defaultInputModes(Collections.singletonList("text")) - .defaultOutputModes(Collections.singletonList("text")) - .skills(Collections.singletonList(AgentSkill.builder() - .id("weather_search") - .name("Search weather") - .description("Helps with weather in cities or states") - .tags(Collections.singletonList("weather")) - .examples(List.of("weather in LA, CA")) - .build())) - .build(); - } -} -``` - -### 3. Add a class that creates an A2A Agent Executor - -```java -import org.a2aproject.sdk.server.agentexecution.AgentExecutor; -import org.a2aproject.sdk.server.agentexecution.RequestContext; -import org.a2aproject.sdk.server.events.EventQueue; -import org.a2aproject.sdk.server.tasks.AgentEmitter; -import org.a2aproject.sdk.spec.JSONRPCError; -import org.a2aproject.sdk.spec.Message; -import org.a2aproject.sdk.spec.Part; -import org.a2aproject.sdk.spec.Task; -import org.a2aproject.sdk.spec.TaskNotCancelableError; -import org.a2aproject.sdk.spec.TaskState; -import org.a2aproject.sdk.spec.TextPart; -... - -@ApplicationScoped -public class WeatherAgentExecutorProducer { - - @Inject - WeatherAgent weatherAgent; - - @Produces - public AgentExecutor agentExecutor() { - return new WeatherAgentExecutor(weatherAgent); - } - - private static class WeatherAgentExecutor implements AgentExecutor { - - private final WeatherAgent weatherAgent; - - public WeatherAgentExecutor(WeatherAgent weatherAgent) { - this.weatherAgent = weatherAgent; - } - - @Override - public void execute(RequestContext context, AgentEmitter agentEmitter) throws JSONRPCError { - // mark the task as submitted and start working on it - if (context.getTask() == null) { - agentEmitter.submit(); - } - agentEmitter.startWork(); - - // extract the text from the message - String userMessage = extractTextFromMessage(context.getMessage()); - - // call the weather agent with the user's message - String response = weatherAgent.chat(userMessage); - - // create the response part - TextPart responsePart = new TextPart(response); - List> parts = List.of(responsePart); - - // add the response as an artifact and complete the task - agentEmitter.addArtifact(parts); - agentEmitter.complete(); - } - - @Override - public void cancel(RequestContext context, AgentEmitter agentEmitter) throws JSONRPCError { - Task task = context.getTask(); - - if (task.getStatus().state() == TaskState.CANCELED) { - // task already cancelled - throw new TaskNotCancelableError(); - } - - if (task.getStatus().state() == TaskState.COMPLETED) { - // task already completed - throw new TaskNotCancelableError(); - } - - // cancel the task - agentEmitter.cancel(); - } - - private String extractTextFromMessage(Message message) { - StringBuilder textBuilder = new StringBuilder(); - for (Part part : message.parts()) { - if (part instanceof TextPart textPart) { - textBuilder.append(textPart.text()); - } - } - return textBuilder.toString(); - } - } -} -``` - -### 4. Configuration System - -The A2A Java SDK uses a flexible configuration system that works across different frameworks. - -**Default behavior:** Configuration values come from `META-INF/a2a-defaults.properties` files on the classpath (provided by core modules and extras). These defaults work out of the box without any additional setup. - -**Customizing configuration:** -- **Quarkus/MicroProfile Config users**: Add the [`microprofile-config`](integrations/microprofile-config/README.md) integration to override defaults via `application.properties`, environment variables, or system properties -- **Spring/other frameworks**: See the [integration module README](integrations/microprofile-config/README.md#custom-config-providers) for how to implement a custom `A2AConfigProvider` -- **Reference implementations**: Already include the MicroProfile Config integration - -#### Configuration Properties - -**Executor Settings** (Optional) - -The SDK uses a dedicated executor for async operations like streaming. Default: 5 core threads, 50 max threads. - -```properties -# Core thread pool size for the @Internal executor (default: 5) -a2a.executor.core-pool-size=5 - -# Maximum thread pool size (default: 50) -a2a.executor.max-pool-size=50 - -# Thread keep-alive time in seconds (default: 60) -a2a.executor.keep-alive-seconds=60 -``` - -**Blocking Call Timeouts** (Optional) - -```properties -# Timeout for agent execution in blocking calls (default: 30 seconds) -a2a.blocking.agent.timeout.seconds=30 - -# Timeout for event consumption in blocking calls (default: 5 seconds) -a2a.blocking.consumption.timeout.seconds=5 -``` - -**Why this matters:** -- **Streaming Performance**: The executor handles streaming subscriptions. Too few threads can cause timeouts under concurrent load. -- **Resource Management**: The dedicated executor prevents streaming operations from competing with the ForkJoinPool. -- **Concurrency**: In production with high concurrent streaming, increase pool sizes accordingly. -- **Agent Timeouts**: LLM-based agents may need longer timeouts (60-120s) compared to simple agents. - -**Note:** The reference server implementations (Quarkus-based) automatically include the MicroProfile Config integration, so properties work out of the box in `application.properties`. - -### 5. Task Authorization (Optional) - -The SDK includes an opt-in SPI for per-user task authorization. When enabled, every `RequestHandler` operation checks whether the authenticated user is allowed to access the target task before proceeding. When no provider is present, all operations are permitted (the default). - -> **⚠ Security note:** For multi-user deployments, a `TaskAuthorizationProvider` **must** be configured. Without one, all operations are permitted regardless of authentication — any authenticated user can read, modify, or cancel any task. Production deployments should use a fail-closed ownership policy (deny access when ownership is unknown). - -#### Providing an implementation - -Create an `@ApplicationScoped` CDI bean that implements `TaskAuthorizationProvider`: - -```java -import jakarta.enterprise.context.ApplicationScoped; -import org.a2aproject.sdk.server.auth.TaskAuthorizationProvider; -import org.a2aproject.sdk.server.auth.TaskOperation; -import org.a2aproject.sdk.server.ServerCallContext; - -@ApplicationScoped -public class MyTaskAuthorizationProvider implements TaskAuthorizationProvider { - - @Override - public boolean checkRead(ServerCallContext context, String taskId, TaskOperation op) { - // Return true to allow, false to deny. - // Denied reads throw TaskNotFoundError — the caller cannot distinguish - // "not found" from "not authorized", preventing information leakage. - String owner = ownershipStore.get(taskId); - if (owner == null) { - return false; // fail-closed: unknown ownership → deny - } - return owner.equals(context.getUser().getUsername()); - } - - @Override - public boolean checkWrite(ServerCallContext context, String taskId, TaskOperation op) { - return checkRead(context, taskId, op); - } - - @Override - public boolean checkCreate(ServerCallContext context, TaskOperation op) { - return context.getUser().isAuthenticated(); - } - - @Override - public boolean isTaskRecorded(String taskId) { - return ownershipStore.contains(taskId); - } - - @Override - public void recordOwnership(ServerCallContext context, String taskId, TaskOperation op) { - ownershipStore.put(taskId, context.getUser().getUsername()); - } -} -``` - -No additional configuration is required — the SDK automatically discovers the bean via CDI and wires it into the request pipeline. See the [`TaskAuthorizationProvider`](server-common/src/main/java/org/a2aproject/sdk/server/auth/TaskAuthorizationProvider.java) javadoc for the full contract, including thread-safety requirements and ownership-recording semantics. - -#### User identity in ServerCallContext - -Authorization decisions rely on `context.getUser()` returning the authenticated user. How the user is populated depends on the transport: - -- **JSON-RPC and REST**: The Quarkus route handler extracts the user from the Vert.x routing context (`rc.userContext()`) and sets it on `ServerCallContext` directly. -- **gRPC**: The reference server includes a `QuarkusCallContextFactory` CDI bean that injects the Quarkus `SecurityIdentity` and maps it to the `ServerCallContext` `User`. This happens automatically when using the reference gRPC module. If you provide your own `CallContextFactory`, you are responsible for populating the user. - -> **Note:** When task authorization is required, always obtain `RequestHandler` through CDI injection. Manual instantiation via `DefaultRequestHandler.create()` bypasses the `AuthorizationRequestHandlerDecorator` and all authorization checks. - -#### How it works - -| Operation | Check | -|-----------|-------| -| `getTask`, `subscribeToTask`, `getTaskPushNotificationConfig`, `listTaskPushNotificationConfigs` | `checkRead` | -| `cancelTask`, `createTaskPushNotificationConfig`, `deleteTaskPushNotificationConfig` | `checkWrite` | -| `messageSend` / `messageSendStream` (existing task) | `checkWrite` | -| `messageSend` / `messageSendStream` (new task) | `checkCreate`, then `recordOwnership` after creation | -| `listTasks` | Filtering pushed to `TaskStore.list()` — calls `checkRead` per task | - -> Task authorization addresses task isolation for deployments that enable `TaskAuthorizationProvider` with a fail-closed ownership policy. Multi-user deployments must configure this as a required security setting, and should avoid policies that allow unknown ownership by default. - -### Serving Older Protocol Versions (Backward Compatibility) - -The A2A Java SDK includes compatibility layers that allow your server to accept requests from clients using older protocol versions. Each compatibility layer is a separate set of modules that you add to your project as needed. **No changes to your `AgentExecutor` are needed** — the compatibility layer converts older protocol requests to v1.0 internally before delegating to your agent. - -#### Adding v0.3 Protocol Support - -To enable v0.3 support, add the v0.3 compat reference module for your chosen transport alongside (or instead of) the v1.0 reference module: - -```xml - - - org.a2aproject.sdk - a2a-java-sdk-compat-0.3-reference-jsonrpc - - ${org.a2aproject.sdk.version} - - - - - org.a2aproject.sdk - a2a-java-sdk-compat-0.3-reference-rest - ${org.a2aproject.sdk.version} - - - - - org.a2aproject.sdk - a2a-java-sdk-compat-0.3-reference-grpc - ${org.a2aproject.sdk.version} - -``` - -For example, a server that supports **v1.0 and v0.3 over JSON-RPC** would include both: - -```xml - - org.a2aproject.sdk - a2a-java-sdk-reference-jsonrpc - ${org.a2aproject.sdk.version} - - - org.a2aproject.sdk - a2a-java-sdk-compat-0.3-reference-jsonrpc - ${org.a2aproject.sdk.version} - -``` - -A server that only needs to support **v0.3** would include only the compat dependency. - -#### Multi-Version Convenience Modules - -For JSON-RPC and REST, multi-version modules are provided that bundle all supported protocol versions together with version-dispatching routes: - -```xml - - - org.a2aproject.sdk - a2a-java-sdk-reference-multiversion-jsonrpc - ${org.a2aproject.sdk.version} - - - - - org.a2aproject.sdk - a2a-java-sdk-reference-multiversion-rest - ${org.a2aproject.sdk.version} - -``` - -These are a convenience — they transitively include all the individual compat reference modules. - -#### How Version Routing Works - -- **JSON-RPC and REST**: When serving multiple protocol versions, version routing inspects the `A2A-Version` HTTP header on each request. If the header is `"1.0"`, the request is routed to the v1.0 handler. If it is `"0.3"` or absent, the request is routed to the v0.3 handler. -- **gRPC**: Version dispatch is implicit — v0.3 clients use the `a2a.v1` protobuf package and v1.0 clients use `lf.a2a.v1`, so requests are routed to the correct service automatically. -- **Agent card**: When both v1.0 and v0.3 are enabled, the v1.0 `AgentCard` takes precedence and is served at `/.well-known/agent-card.json`. The v0.3 `AgentCard_v0_3` is ignored. If only v0.3 is enabled, the v0.3 agent card is used. If only v1.0 is enabled, the v1.0 agent card is used as-is. - -#### Making the v1.0 Agent Card Compatible with v0.3 Clients - -When serving both protocol versions, you need to ensure the v1.0 agent card contains fields that v0.3 clients expect. Existing v0.3 client implementations (in any language) look for `url`, `preferredTransport`, and `additionalInterfaces` with `transport`/`url` entries — fields that don't exist in the v1.0 format by default. - -To make your v1.0 `AgentCard` parsable by v0.3 clients, set these fields on the builder: - -```java -AgentCard card = AgentCard.builder() - .name("My Agent") - // ... other v1.0 fields ... - .supportedInterfaces(List.of( - new AgentInterface("jsonrpc", "http://localhost:9999"))) - // v0.3 backward-compatibility fields: - .url("http://localhost:9999") - .preferredTransport("jsonrpc") - .additionalInterfaces(List.of( - new Legacy_0_3_AgentInterface("jsonrpc", "http://localhost:9999"))) - .build(); -``` - -The two interface lists serve different clients: - -- `supportedInterfaces` — used by **v1.0 clients** to discover endpoints (uses `AgentInterface` with `protocolBinding`/`url`/`tenant` fields) -- `additionalInterfaces` — used by **v0.3 clients** to discover endpoints (uses `Legacy_0_3_AgentInterface` with v0.3 field names: `transport`/`url`) -- `url` and `preferredTransport` — top-level fields that v0.3 clients use to discover the primary endpoint - -#### Push Notification Behavior +See the [Server Guide](https://a2aproject.github.io/a2a-java/server) for full setup instructions. -Push notification payloads are automatically formatted to match the protocol version used when the push notification configuration was registered. When a v0.3 client registers a push notification configuration (via any transport), the server records the protocol version alongside the configuration. When a notification is later sent to that webhook, the payload is formatted as a v0.3 Task object. Configurations registered by v1.0 clients receive v1.0 `StreamResponse` payloads as usual. This happens transparently — no additional configuration is needed beyond adding the compat reference module. +## Build -## A2A Client - -The A2A Java SDK provides a Java client implementation of the [Agent2Agent (A2A) Protocol](https://a2a-protocol.org/), allowing communication with A2A servers. The Java client implementation supports the following transports: - -* JSON-RPC 2.0 -* gRPC -* HTTP+JSON/REST - -To make use of the Java `Client`: - -### 1. Add the A2A Java SDK Client dependency to your project - -Adding a dependency on `a2a-java-sdk-client` will provide access to a `ClientBuilder` -that you can use to create your A2A `Client`. - -```xml - - org.a2aproject.sdk - a2a-java-sdk-client - - ${org.a2aproject.sdk.version} - -``` - -### 2. Add one or more dependencies on the A2A Java SDK Client Transport(s) you'd like to use - -By default, the `sdk-client` artifact includes the JSONRPC transport dependency. However, you must still explicitly configure this transport when building the `Client` as described in the [JSON-RPC Transport section](#json-rpc-transport-configuration). - - -If you want to use the gRPC transport, you'll need to add a relevant dependency: - -```xml - - org.a2aproject.sdk - a2a-java-sdk-client-transport-grpc - - ${org.a2aproject.sdk.version} - -``` - - -If you want to use the HTTP+JSON/REST transport, you'll need to add a relevant dependency: - - -```xml - - org.a2aproject.sdk - a2a-java-sdk-client-transport-rest - - ${org.a2aproject.sdk.version} - -``` - -### Sample Usage - -#### Create a Client using the ClientBuilder - -```java -// First, get the agent card for the A2A server agent you want to connect to -AgentCard agentCard = A2ACardResolver.builder().baseUrl("http://localhost:1234").build().getAgentCard(); - -// Specify configuration for the ClientBuilder -ClientConfig clientConfig = new ClientConfig.Builder() - .setAcceptedOutputModes(List.of("text")) - .build(); - -// Create event consumers to handle responses that will be received from the A2A server -// (these consumers will be used for both streaming and non-streaming responses) -List> consumers = List.of( - (event, card) -> { - if (event instanceof MessageEvent messageEvent) { - // handle the messageEvent.getMessage() - ... - } else if (event instanceof TaskEvent taskEvent) { - // handle the taskEvent.getTask() - ... - } else if (event instanceof TaskUpdateEvent updateEvent) { - // handle the updateEvent.getTask() - ... - } - } -); - -// Create a handler that will be used for any errors that occur during streaming -Consumer errorHandler = error -> { - // handle the error.getMessage() - ... -}; - -// Create the client using the builder -Client client = Client - .builder(agentCard) - .clientConfig(clientConfig) - .withTransport(JSONRPCTransport.class, new JSONRPCTransportConfig()) - .addConsumers(consumers) - .streamingErrorHandler(errorHandler) - .build(); -``` - -#### Configuring Transport-Specific Settings - -Different transport protocols can be configured with specific settings using specific `ClientTransportConfig` implementations. The A2A Java SDK provides `JSONRPCTransportConfig` for the JSON-RPC transport and `GrpcTransportConfig` for the gRPC transport. - -##### JSON-RPC Transport Configuration - -For the JSON-RPC transport, to use the default `JdkA2AHttpClient`, provide a `JSONRPCTransportConfig` created with its default constructor. - -To use a custom HTTP client implementation, simply create a `JSONRPCTransportConfig` as follows: - -```java -// Create a custom HTTP client -A2AHttpClient customHttpClient = ... - -// Configure the client settings -ClientConfig clientConfig = new ClientConfig.Builder() - .setAcceptedOutputModes(List.of("text")) - .build(); - -Client client = Client - .builder(agentCard) - .clientConfig(clientConfig) - .withTransport(JSONRPCTransport.class, new JSONRPCTransportConfig(customHttpClient)) - .build(); -``` - -To customize the default JDK HTTP client without replacing the SDK implementation, provide -your own `java.net.http.HttpClient` to `JdkA2AHttpClient`: - -```java -HttpClient jdkHttpClient = HttpClient.newBuilder() - .connectTimeout(Duration.ofSeconds(5)) - .followRedirects(HttpClient.Redirect.NORMAL) - .version(HttpClient.Version.HTTP_2) - .build(); - -Client client = Client - .builder(agentCard) - .withTransport(JSONRPCTransport.class, new JSONRPCTransportConfig( - new JdkA2AHttpClient(jdkHttpClient))) - .build(); -``` - -##### gRPC Transport Configuration - -For the gRPC transport, you must configure a channel factory: - -```java -// Create a channel factory function that takes the agent URL and returns a Channel -Function channelFactory = agentUrl -> { - return ManagedChannelBuilder.forTarget(agentUrl) - ... - .build(); -}; - -// Configure the client with transport-specific settings -ClientConfig clientConfig = new ClientConfig.Builder() - .setAcceptedOutputModes(List.of("text")) - .build(); - -Client client = Client - .builder(agentCard) - .clientConfig(clientConfig) - .withTransport(GrpcTransport.class, new GrpcTransportConfig(channelFactory)) - .build(); -``` - - -##### HTTP+JSON/REST Transport Configuration - -For the HTTP+JSON/REST transport, if you'd like to use the default `JdkA2AHttpClient`, provide a `RestTransportConfig` created with its default constructor. - -To use a custom HTTP client implementation, simply create a `RestTransportConfig` as follows: - -```java -// Create a custom HTTP client -A2AHttpClient customHttpClient = ... - -// Configure the client settings -ClientConfig clientConfig = new ClientConfig.Builder() - .setAcceptedOutputModes(List.of("text")) - .build(); - -Client client = Client - .builder(agentCard) - .clientConfig(clientConfig) - .withTransport(RestTransport.class, new RestTransportConfig(customHttpClient)) - .build(); -``` - -##### Multiple Transport Configurations - -You can specify configuration for multiple transports, the appropriate configuration -will be used based on the selected transport: - -```java -// Configure both JSON-RPC and gRPC transports -Client client = Client - .builder(agentCard) - .withTransport(GrpcTransport.class, new GrpcTransportConfig(channelFactory)) - .withTransport(JSONRPCTransport.class, new JSONRPCTransportConfig()) - .withTransport(RestTransport.class, new RestTransportConfig()) - .build(); -``` - -#### Send a message to the A2A server agent - -```java -// Send a text message to the A2A server agent -Message message = A2A.toUserMessage("tell me a joke"); - -// Send the message (uses configured consumers to handle responses) -// Streaming will automatically be used if supported by both client and server, -// otherwise the non-streaming send message method will be used automatically -client.sendMessage(message); - -// You can also optionally specify a ClientCallContext with call-specific config to use -client.sendMessage(message, clientCallContext); -``` - -#### Send a message with custom event handling - -```java -// Create custom consumers for this specific message -List> customConsumers = List.of( - (event, card) -> { - // handle this specific message's responses - ... - } -); - -// Create custom error handler -Consumer customErrorHandler = error -> { - // handle the error - ... -}; - -Message message = A2A.toUserMessage("tell me a joke"); -client.sendMessage(message, customConsumers, customErrorHandler); -``` - -#### Get the current state of a task - -```java -// Retrieve the task with id "task-1234" -Task task = client.getTask(new TaskQueryParams("task-1234")); - -// You can also specify the maximum number of history items for the task -// to include in the response -Task task = client.getTask(new TaskQueryParams("task-1234", 10)); - -// You can also optionally specify a ClientCallContext with call-specific config to use -Task task = client.getTask(new TaskQueryParams("task-1234"), clientCallContext); -``` - -#### Cancel an ongoing task - -```java -// Cancel the task we previously submitted with id "task-1234" -Task cancelledTask = client.cancelTask(new TaskIdParams("task-1234")); - -// You can also specify additional properties using a map -Map metadata = Map.of("reason", "user_requested"); -Task cancelledTask = client.cancelTask(new TaskIdParams("task-1234", metadata)); - -// You can also optionally specify a ClientCallContext with call-specific config to use -Task cancelledTask = client.cancelTask(new TaskIdParams("task-1234"), clientCallContext); -``` - -#### Get a push notification configuration for a task - -```java -// Get task push notification configuration -TaskPushNotificationConfig config = client.getTaskPushNotificationConfiguration( - new GetTaskPushNotificationConfigParams("task-1234")); - -// The push notification configuration ID can also be optionally specified -TaskPushNotificationConfig config = client.getTaskPushNotificationConfiguration( - new GetTaskPushNotificationConfigParams("task-1234", "config-4567")); - -// Additional properties can be specified using a map -Map metadata = Map.of("source", "client"); -TaskPushNotificationConfig config = client.getTaskPushNotificationConfiguration( - new GetTaskPushNotificationConfigParams("task-1234", "config-1234", metadata)); - -// You can also optionally specify a ClientCallContext with call-specific config to use -TaskPushNotificationConfig config = client.getTaskPushNotificationConfiguration( - new GetTaskPushNotificationConfigParams("task-1234"), clientCallContext); -``` - -#### Set a push notification configuration for a task - -```java -// Set task push notification configuration -PushNotificationConfig pushNotificationConfig = PushNotificationConfig.builder() - .url("https://example.com/callback") - .authenticationInfo(new AuthenticationInfo(Collections.singletonList("jwt"), null)) - .build(); - -TaskPushNotificationConfig taskConfig = TaskPushNotificationConfig.builder() - .taskId("task-1234") - .pushNotificationConfig(pushNotificationConfig) - .build(); - -TaskPushNotificationConfig result = client.createTaskPushNotificationConfiguration(taskConfig); - -// You can also optionally specify a ClientCallContext with call-specific config to use -TaskPushNotificationConfig result = client.createTaskPushNotificationConfiguration(taskConfig, clientCallContext); -``` - -#### List the push notification configurations for a task - -```java -List configs = client.listTaskPushNotificationConfigurations( - new ListTaskPushNotificationConfigParams("task-1234")); - -// Additional properties can be specified using a map -Map metadata = Map.of("filter", "active"); -List configs = client.listTaskPushNotificationConfigurations( - new ListTaskPushNotificationConfigParams("task-1234", metadata)); - -// You can also optionally specify a ClientCallContext with call-specific config to use -List configs = client.listTaskPushNotificationConfigurations( - new ListTaskPushNotificationConfigParams("task-1234"), clientCallContext); -``` - -#### Delete a push notification configuration for a task - -```java -client.deleteTaskPushNotificationConfigurations( - new DeleteTaskPushNotificationConfigParams("task-1234", "config-4567")); - -// Additional properties can be specified using a map -Map metadata = Map.of("reason", "cleanup"); -client.deleteTaskPushNotificationConfigurations( - new DeleteTaskPushNotificationConfigParams("task-1234", "config-4567", metadata)); - -// You can also optionally specify a ClientCallContext with call-specific config to use -client.deleteTaskPushNotificationConfigurations( - new DeleteTaskPushNotificationConfigParams("task-1234", "config-4567", clientCallContext); -``` - -#### Subscribe to a task - -```java -// Subscribe to an ongoing task with id "task-1234" using configured consumers -TaskIdParams taskIdParams = new TaskIdParams("task-1234"); -client.subscribeToTask(taskIdParams); - -// Or subscribe with custom consumers and error handler -List> customConsumers = List.of( - (event, card) -> System.out.println("Subscribe event: " + event) -); -Consumer customErrorHandler = error -> - System.err.println("Subscribe error: " + error.getMessage()); - -client.subscribeToTask(taskIdParams, customConsumers, customErrorHandler); - -// You can also optionally specify a ClientCallContext with call-specific config to use -client.subscribeToTask(taskIdParams, clientCallContext); -``` - -#### Retrieve details about the server agent that this client agent is communicating with -```java -AgentCard serverAgentCard = client.getAgentCard(); -``` - -### Communicating with v0.3 Agents - -If you need to communicate with an agent that only supports protocol v0.3, use the compatibility client `Client_v0_3`. - -#### 1. Add the v0.3 client dependency - -```xml - - org.a2aproject.sdk - a2a-java-sdk-compat-0.3-client - - ${org.a2aproject.sdk.version} - +```bash +mvn clean install ``` -#### 2. Add a v0.3 client transport dependency +### Regeneration of gRPC files -```xml - - - org.a2aproject.sdk - a2a-java-sdk-compat-0.3-client-transport-jsonrpc - ${org.a2aproject.sdk.version} - +We copy https://github.com/a2aproject/A2A/blob/main/specification/grpc/a2a.proto to the [`spec-grpc/`](./spec-grpc) project, and adjust the `java_package` option to be as follows: ``` - -gRPC and REST transports are also available: -- `a2a-java-sdk-compat-0.3-client-transport-grpc` -- `a2a-java-sdk-compat-0.3-client-transport-rest` - -#### 3. Create the v0.3 client - -```java -AgentCard card = A2ACardResolver.builder().baseUrl("http://localhost:1234").build().getAgentCard(); - -// Find the v0.3 interface from the agent card -AgentInterface v03Interface = card.supportedInterfaces().stream() - .filter(iface -> A2AProtocol_v0_3.PROTOCOL_VERSION.equals(iface.protocolVersion())) - .findFirst() - .orElseThrow(); - -// Create the v0.3 compatibility client -Client_v0_3 client = ClientBuilder_v0_3.forUrl(v03Interface.url()) - .withTransport(JSONRPCTransport_v0_3.class, new JSONRPCTransportConfigBuilder_v0_3()) - .build(); +option java_package = "org.a2aproject.sdk.grpc"; ``` +Then build the `spec-grpc` module with `mvn clean install -Dskip.protobuf.generate=false` to regenerate the gRPC classes in the `org.a2aproject.sdk.grpc` package. -**Note:** `Client_v0_3` exposes only operations available in protocol v0.3. For example, `listTasks()` is not available (it was added in v1.0). Return types use v0.3 domain objects from the `org.a2aproject.sdk.compat03.spec` package. - -## Additional Examples - -### Hello World Client Example - -A complete example of a Java A2A client communicating with a Python A2A server is available in the [examples/helloworld/client](examples/helloworld/client/README.md) directory. This example demonstrates: - -- Setting up and using the A2A Java client -- Sending regular and streaming messages to a Python A2A server -- Receiving and processing responses from the Python A2A server - -The example includes detailed instructions on how to run the Python A2A server and how to run the Java A2A client using JBang. +## Examples -Check out the [example's README](examples/helloworld/client/README.md) for more information. +- [Hello World Server](examples/helloworld/server/README.md) — Java A2A server with a Python client +- [Hello World Client](examples/helloworld/client/README.md) — Java A2A client with a Python server +- [a2a-samples](https://github.com/a2aproject/a2a-samples/tree/main/samples/java/agents) — More agent examples -### Hello World Server Example +## Server Integrations -A complete example of a Python A2A client communicating with a Java A2A server is available in the [examples/helloworld/server](examples/helloworld/server/README.md) directory. This example demonstrates: +Community contributed integrations with various Java runtimes: -- A sample `AgentCard` producer -- A sample `AgentExecutor` producer -- A Java A2A server receiving regular and streaming messages from a Python A2A client +* **Quarkus** — This project contains the reference implementations for JSON-RPC, gRPC, and HTTP+JSON (REST) transports. +* **Jakarta EE** — [a2a-jakarta](https://github.com/wildfly-extras/a2a-jakarta) works with any runtime supporting the [Jakarta EE Web Profile](https://jakarta.ee/specifications/webprofile/). -Check out the [example's README](examples/helloworld/server/README.md) for more information. +To contribute an integration, see [CONTRIBUTING_INTEGRATIONS.md](CONTRIBUTING_INTEGRATIONS.md). -## Community Articles +## Extras -See [COMMUNITY_ARTICLES.md](COMMUNITY_ARTICLES.md) for a list of community articles and videos. +See the [`extras`](./extras/README.md) folder for optional add-ons (OpenTelemetry, JPA-backed task store, Kafka-based replicated queue manager, Vert.x and Android HTTP clients). ## License @@ -903,15 +80,3 @@ This project is licensed under the terms of the [Apache 2.0 License](LICENSE). ## Contributing See [CONTRIBUTING.md](CONTRIBUTING.md) for contribution guidelines. - -## Server Integrations -The following list contains community contributed integrations with various Java Runtimes. - -To contribute an integration, please see [CONTRIBUTING_INTEGRATIONS.md](CONTRIBUTING_INTEGRATIONS.md). - -* This project contains integration with Quarkus and has the reference implementations for the JSON-RPC, gRPC, and HTTP+JSON (REST) transports. -* https://github.com/wildfly-extras/a2a-jakarta - This integration is based on Jakarta EE, and should work in all runtimes supporting the [Jakarta EE Web Profile](https://jakarta.ee/specifications/webprofile/). - -# Extras -See the [`extras`](./extras/README.md) folder for extra functionality not provided by the SDK itself! - diff --git a/docs/content/client.md b/docs/content/client.md index 7083ac2a4..94ca0492b 100644 --- a/docs/content/client.md +++ b/docs/content/client.md @@ -122,6 +122,10 @@ TaskPushNotificationConfig taskConfig = TaskPushNotificationConfig.builder() client.createTaskPushNotificationConfiguration(taskConfig); +// Get a specific configuration +TaskPushNotificationConfig config = client.getTaskPushNotificationConfiguration( + new GetTaskPushNotificationConfigParams("task-1234", "config-4567")); + // List configurations List configs = client.listTaskPushNotificationConfigurations( @@ -150,6 +154,17 @@ Client client = Client .build(); ``` +### REST with a Custom HTTP Client + +```java +A2AHttpClient customHttpClient = ... + +Client client = Client + .builder(agentCard) + .withTransport(RestTransport.class, new RestTransportConfig(customHttpClient)) + .build(); +``` + ### gRPC ```java @@ -190,6 +205,10 @@ Use `Client_v0_3` to communicate with agents that only support protocol v0.3: ``` +gRPC and REST transports are also available: +- `a2a-java-sdk-compat-0.3-client-transport-grpc` +- `a2a-java-sdk-compat-0.3-client-transport-rest` + ```java AgentCard card = A2ACardResolver.builder().baseUrl("http://localhost:1234") .build().getAgentCard(); @@ -203,6 +222,8 @@ Client_v0_3 client = ClientBuilder_v0_3.forUrl(v03Interface.url()) .build(); ``` +**Note:** `Client_v0_3` exposes only operations available in protocol v0.3. For example, `listTasks()` is not available (it was added in v1.0). Return types use v0.3 domain objects from the `org.a2aproject.sdk.compat03.spec` package. + ## Examples - [Hello World Client](https://github.com/a2aproject/a2a-java/blob/main/examples/helloworld/client/README.md) — Java client talking to a Python A2A server diff --git a/docs/content/server.md b/docs/content/server.md index f74708820..315e8bd71 100644 --- a/docs/content/server.md +++ b/docs/content/server.md @@ -152,23 +152,54 @@ public class WeatherAgentExecutorProducer { ## 4. Configuration -The SDK uses `META-INF/a2a-defaults.properties` for defaults. Override via `application.properties` when using Quarkus/MicroProfile Config: +The A2A Java SDK uses a flexible configuration system that works across different frameworks. + +**Default behavior:** Configuration values come from `META-INF/a2a-defaults.properties` files on the classpath (provided by core modules and extras). These defaults work out of the box without any additional setup. + +**Customizing configuration:** +- **Quarkus/MicroProfile Config users**: Add the [`microprofile-config`](https://github.com/a2aproject/a2a-java/blob/main/integrations/microprofile-config/README.md) integration to override defaults via `application.properties`, environment variables, or system properties +- **Spring/other frameworks**: See the [integration module README](https://github.com/a2aproject/a2a-java/blob/main/integrations/microprofile-config/README.md#custom-config-providers) for how to implement a custom `A2AConfigProvider` +- **Reference implementations**: Already include the MicroProfile Config integration + +### Configuration Properties + +**Executor Settings** (Optional) + +The SDK uses a dedicated executor for async operations like streaming. Default: 5 core threads, 50 max threads. ```properties -# Thread pool for async/streaming operations +# Core thread pool size for the @Internal executor (default: 5) a2a.executor.core-pool-size=5 + +# Maximum thread pool size (default: 50) a2a.executor.max-pool-size=50 + +# Thread keep-alive time in seconds (default: 60) a2a.executor.keep-alive-seconds=60 +``` -# Timeouts for blocking calls +**Blocking Call Timeouts** (Optional) + +```properties +# Timeout for agent execution in blocking calls (default: 30 seconds) a2a.blocking.agent.timeout.seconds=30 + +# Timeout for event consumption in blocking calls (default: 5 seconds) a2a.blocking.consumption.timeout.seconds=5 ``` -For LLM-based agents, increase `a2a.blocking.agent.timeout.seconds` to 60–120 seconds. +**Why this matters:** +- **Streaming Performance**: The executor handles streaming subscriptions. Too few threads can cause timeouts under concurrent load. +- **Resource Management**: The dedicated executor prevents streaming operations from competing with the ForkJoinPool. +- **Concurrency**: In production with high concurrent streaming, increase pool sizes accordingly. +- **Agent Timeouts**: LLM-based agents may need longer timeouts (60–120s) compared to simple agents. + +**Note:** The reference server implementations (Quarkus-based) automatically include the MicroProfile Config integration, so properties work out of the box in `application.properties`. ## 5. Task Authorization (Optional) +> **⚠ Security note:** For multi-user deployments, a `TaskAuthorizationProvider` **must** be configured. Without one, all operations are permitted regardless of authentication — any authenticated user can read, modify, or cancel any task. Production deployments should use a fail-closed ownership policy (deny access when ownership is unknown). + Implement `TaskAuthorizationProvider` to control per-user access: ```java @@ -204,6 +235,17 @@ public class MyTaskAuthorizationProvider implements TaskAuthorizationProvider { The SDK discovers the bean via CDI automatically — no additional wiring needed. +> **Note:** When task authorization is required, always obtain `RequestHandler` through CDI injection. Manual instantiation via `DefaultRequestHandler.create()` bypasses the `AuthorizationRequestHandlerDecorator` and all authorization checks. + +### User Identity in ServerCallContext + +Authorization decisions rely on `context.getUser()` returning the authenticated user. How the user is populated depends on the transport: + +- **JSON-RPC and REST**: The Quarkus route handler extracts the user from the Vert.x routing context (`rc.userContext()`) and sets it on `ServerCallContext` directly. +- **gRPC**: The reference server includes a `QuarkusCallContextFactory` CDI bean that injects the Quarkus `SecurityIdentity` and maps it to the `ServerCallContext` `User`. This happens automatically when using the reference gRPC module. If you provide your own `CallContextFactory`, you are responsible for populating the user. + +### Authorization Checks + | Operation | Authorization check | |-----------|---------------------| | `getTask`, `subscribeToTask`, `getTaskPushNotificationConfig`, `listTaskPushNotificationConfigs` | `checkRead` | @@ -237,14 +279,63 @@ Add compat modules alongside v1.0 modules to serve both protocol versions simult ### Individual Compat Modules ```xml + org.a2aproject.sdk a2a-java-sdk-compat-0.3-reference-jsonrpc $\{org.a2aproject.sdk.version} + + + + org.a2aproject.sdk + a2a-java-sdk-compat-0.3-reference-rest + $\{org.a2aproject.sdk.version} + + + + + org.a2aproject.sdk + a2a-java-sdk-compat-0.3-reference-grpc + $\{org.a2aproject.sdk.version} + ``` -Version routing uses the `A2A-Version` HTTP header for JSON-RPC and REST; for gRPC it is implicit via protobuf package name. +### How Version Routing Works + +- **JSON-RPC and REST**: When serving multiple protocol versions, version routing inspects the `A2A-Version` HTTP header on each request. If the header is `"1.0"`, the request is routed to the v1.0 handler. If it is `"0.3"` or absent, the request is routed to the v0.3 handler. +- **gRPC**: Version dispatch is implicit — v0.3 clients use the `a2a.v1` protobuf package and v1.0 clients use `lf.a2a.v1`, so requests are routed to the correct service automatically. +- **Agent card**: When both v1.0 and v0.3 are enabled, the v1.0 `AgentCard` takes precedence and is served at `/.well-known/agent-card.json`. The v0.3 `AgentCard_v0_3` is ignored. If only v0.3 is enabled, the v0.3 agent card is used. If only v1.0 is enabled, the v1.0 agent card is used as-is. + +### Making the v1.0 Agent Card Compatible with v0.3 Clients + +When serving both protocol versions, you need to ensure the v1.0 agent card contains fields that v0.3 clients expect. Existing v0.3 client implementations (in any language) look for `url`, `preferredTransport`, and `additionalInterfaces` with `transport`/`url` entries — fields that don't exist in the v1.0 format by default. + +To make your v1.0 `AgentCard` parsable by v0.3 clients, set these fields on the builder: + +```java +AgentCard card = AgentCard.builder() + .name("My Agent") + // ... other v1.0 fields ... + .supportedInterfaces(List.of( + new AgentInterface("jsonrpc", "http://localhost:9999"))) + // v0.3 backward-compatibility fields: + .url("http://localhost:9999") + .preferredTransport("jsonrpc") + .additionalInterfaces(List.of( + new Legacy_0_3_AgentInterface("jsonrpc", "http://localhost:9999"))) + .build(); +``` + +The two interface lists serve different clients: + +- `supportedInterfaces` — used by **v1.0 clients** to discover endpoints (uses `AgentInterface` with `protocolBinding`/`url`/`tenant` fields) +- `additionalInterfaces` — used by **v0.3 clients** to discover endpoints (uses `Legacy_0_3_AgentInterface` with v0.3 field names: `transport`/`url`) +- `url` and `preferredTransport` — top-level fields that v0.3 clients use to discover the primary endpoint + +### Push Notification Behavior + +Push notification payloads are automatically formatted to match the protocol version used when the push notification configuration was registered. When a v0.3 client registers a push notification configuration (via any transport), the server records the protocol version alongside the configuration. When a notification is later sent to that webhook, the payload is formatted as a v0.3 Task object. Configurations registered by v1.0 clients receive v1.0 `StreamResponse` payloads as usual. This happens transparently — no additional configuration is needed beyond adding the compat reference module. ## Server Integrations