diff --git a/AGENTS.md b/AGENTS.md index 144c0751b..8475aa3b7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -74,6 +74,10 @@ mvn clean install - Try to use existing code instead of generating new similar code - Use the same code convention than existing code. If the existing convention seems incorrect, make suggestion before doing any changes +### Documentation Site + +The docs site (`docs/`) uses versioned content folders (`docs/content/1.0.0.Final/`, `docs/content/1.1.0.Final/`, `docs/content/dev/`). When updating documentation to reflect code changes, only edit pages under `docs/content/dev/` — released version folders are frozen snapshots and must not be modified. New versioned folders are created at release time (see RELEASE.md). + ### PR instructions - Follow the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/#summary) for the commit title and message - Always ask if the commit is related to a GitHub issue. If that's the case, add a `This fixes #{issue_number}` at the end of the commit message diff --git a/RELEASE.md b/RELEASE.md index c866ca78a..c08a143d9 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -161,7 +161,32 @@ Artifacts should include: - `.pom` files - `.asc` GPG signatures for all artifacts -### 9. Increment to Next SNAPSHOT +### 9. Update Versioned Documentation + +Create a new documentation version for the release: + +```bash +# Copy dev docs to the new version folder +cp -r docs/content/dev docs/content/X.Y.Z.Final + +# Create a new version data file +cp docs/data/versions/dev.yml docs/data/versions/X.Y.Z.Final.yml +``` + +Edit `docs/data/versions/X.Y.Z.Final.yml`: +- Set `label` to `"X.Y.Z.Final"` +- Set `path` to `"X.Y.Z.Final"` +- Set `sortOrder` to the next number (higher than the previous release) +- Set `defaultVersion` to `true` +- Set `devVersion` to `false` +- Adjust the `menu` list if the new version adds or removes pages + +Update the previous default version's data file (e.g., `docs/data/versions/OLD_VERSION.yml`): +- Set `defaultVersion` to `false` + +Review the new version's content for accuracy — ensure all pages reflect features available in this release. + +### 10. Increment to Next SNAPSHOT Prepare repository for next development cycle: diff --git a/boms/extras/src/it/extras-usage-test/src/main/java/org/a2aproject/sdk/bom/test/extras/ExtrasBomVerifier.java b/boms/extras/src/it/extras-usage-test/src/main/java/org/a2aproject/sdk/bom/test/extras/ExtrasBomVerifier.java index 09080f412..8828bc873 100644 --- a/boms/extras/src/it/extras-usage-test/src/main/java/org/a2aproject/sdk/bom/test/extras/ExtrasBomVerifier.java +++ b/boms/extras/src/it/extras-usage-test/src/main/java/org/a2aproject/sdk/bom/test/extras/ExtrasBomVerifier.java @@ -13,6 +13,7 @@ public class ExtrasBomVerifier extends DynamicBomVerifier { private static final Set EXTRAS_EXCLUSIONS = Set.of( "boms/", // BOM test modules themselves + "docs/", // Documentation site (not an SDK artifact) "examples/", // Example applications "itk/", // Integration Test Kit agent "tck/", // TCK test suite diff --git a/boms/reference/src/it/reference-usage-test/src/main/java/org/a2aproject/sdk/bom/test/reference/ReferenceBomVerifier.java b/boms/reference/src/it/reference-usage-test/src/main/java/org/a2aproject/sdk/bom/test/reference/ReferenceBomVerifier.java index f9dec0edc..0c7caff97 100644 --- a/boms/reference/src/it/reference-usage-test/src/main/java/org/a2aproject/sdk/bom/test/reference/ReferenceBomVerifier.java +++ b/boms/reference/src/it/reference-usage-test/src/main/java/org/a2aproject/sdk/bom/test/reference/ReferenceBomVerifier.java @@ -13,6 +13,7 @@ public class ReferenceBomVerifier extends DynamicBomVerifier { private static final Set REFERENCE_EXCLUSIONS = Set.of( "boms/", // BOM test modules themselves + "docs/", // Documentation site (not an SDK artifact) "examples/", // Example applications "itk/", // Integration Test Kit agent "tck/", // TCK test suite diff --git a/boms/sdk/src/it/sdk-usage-test/src/main/java/org/a2aproject/sdk/bom/test/sdk/SdkBomVerifier.java b/boms/sdk/src/it/sdk-usage-test/src/main/java/org/a2aproject/sdk/bom/test/sdk/SdkBomVerifier.java index 10e72e618..5b79e78b4 100644 --- a/boms/sdk/src/it/sdk-usage-test/src/main/java/org/a2aproject/sdk/bom/test/sdk/SdkBomVerifier.java +++ b/boms/sdk/src/it/sdk-usage-test/src/main/java/org/a2aproject/sdk/bom/test/sdk/SdkBomVerifier.java @@ -13,6 +13,7 @@ public class SdkBomVerifier extends DynamicBomVerifier { private static final Set SDK_EXCLUSIONS = Set.of( "boms/", // BOM test modules themselves + "docs/", // Documentation site (not an SDK artifact) "examples/", // Example applications "itk/", // Integration Test Kit agent "tck/", // TCK test suite diff --git a/docs/content/boms.md b/docs/content/1.0.0.Final/boms.md similarity index 100% rename from docs/content/boms.md rename to docs/content/1.0.0.Final/boms.md diff --git a/docs/content/client.md b/docs/content/1.0.0.Final/client.md similarity index 95% rename from docs/content/client.md rename to docs/content/1.0.0.Final/client.md index ffa5b67f1..c39bb24f9 100644 --- a/docs/content/client.md +++ b/docs/content/1.0.0.Final/client.md @@ -190,8 +190,8 @@ Client client = Client ## Communicating with v0.3 Agents -See [Backward Compatibility]({site.url('compatibility')}#client-communicating-with-v03-agents) for using `Client_v0_3` with older protocol agents. +See [Backward Compatibility](compatibility#client-communicating-with-v03-agents) for using `Client_v0_3` with older protocol agents. ## Examples -See [Examples]({site.url('examples')}) for Hello World walkthroughs and sample applications. +See [Examples](examples) for Hello World walkthroughs and sample applications. diff --git a/docs/content/compatibility.md b/docs/content/1.0.0.Final/compatibility.md similarity index 100% rename from docs/content/compatibility.md rename to docs/content/1.0.0.Final/compatibility.md diff --git a/docs/content/configuration.md b/docs/content/1.0.0.Final/configuration.md similarity index 100% rename from docs/content/configuration.md rename to docs/content/1.0.0.Final/configuration.md diff --git a/docs/content/examples.md b/docs/content/1.0.0.Final/examples.md similarity index 97% rename from docs/content/examples.md rename to docs/content/1.0.0.Final/examples.md index 369d1bd73..9d5b0fb40 100644 --- a/docs/content/examples.md +++ b/docs/content/1.0.0.Final/examples.md @@ -142,7 +142,7 @@ mvn exec:java -Dopentelemetry=true The client expects an OpenTelemetry collector on port 5317. The easiest way is to run the Java server with `-Popentelemetry` (which starts the collector automatically), then run the client with `-Dopentelemetry=true` for end-to-end traces. -For more information, see the [OpenTelemetry extras module]({site.url('/extras/opentelemetry')}). +For more information, see the [OpenTelemetry extras module](extras/opentelemetry). ## More Examples diff --git a/docs/content/extras.md b/docs/content/1.0.0.Final/extras.md similarity index 91% rename from docs/content/extras.md rename to docs/content/1.0.0.Final/extras.md index 8c02ae1aa..2a4478b5e 100644 --- a/docs/content/extras.md +++ b/docs/content/1.0.0.Final/extras.md @@ -28,18 +28,18 @@ Import the extras BOM to manage versions: ``` -## [HTTP Clients]({site.url('/extras/http-clients')}) +## [HTTP Clients](extras/http-clients) Alternative `A2AHttpClient` implementations. The **Vert.x HTTP Client** (`a2a-java-sdk-http-client-vertx`) replaces the default JDK HttpClient with a reactive, non-blocking Vert.x WebClient — recommended for Quarkus, reactive frameworks, and high-throughput scenarios. An **Android HTTP Client** (`a2a-java-sdk-http-client-android`) is also available for mobile applications. -## [Storage & Persistence]({site.url('/extras/storage')}) +## [Storage & Persistence](extras/storage) JPA-backed replacements for the in-memory stores, providing database persistence for production and load-balanced deployments. Includes a **JPA Task Store** (`a2a-java-extras-task-store-database-jpa`) and a **JPA Push Notification Config Store** (`a2a-java-extras-push-notification-config-store-database-jpa`). Both use Jakarta Persistence API (JPA 3.0+) and share the same datasource configuration. -## [Replicated Queue Manager]({site.url('/extras/replicated-queue-manager')}) +## [Replicated Queue Manager](extras/replicated-queue-manager) Replaces the default `InMemoryQueueManager` with event replication across multiple A2A server instances via message brokers. Required for multi-instance deployments. The core module (`a2a-java-queue-manager-replicated-core`) pairs with a MicroProfile Reactive Messaging strategy (`a2a-java-queue-manager-replication-mp-reactive`) supporting Apache Kafka, Pulsar, or AMQP. You can also write your own `ReplicationStrategy`. -## [OpenTelemetry]({site.url('/extras/opentelemetry')}) +## [OpenTelemetry](extras/opentelemetry) Distributed tracing, metrics, and context propagation for A2A servers and clients using OpenTelemetry. The server module (`a2a-java-sdk-opentelemetry-server`) adds automatic span creation for all protocol methods with context propagation across async boundaries. Client modules (`a2a-java-sdk-opentelemetry-client`, `a2a-java-sdk-opentelemetry-client-propagation`) instrument A2A client operations. diff --git a/docs/content/extras/http-clients.md b/docs/content/1.0.0.Final/extras/http-clients.md similarity index 100% rename from docs/content/extras/http-clients.md rename to docs/content/1.0.0.Final/extras/http-clients.md diff --git a/docs/content/extras/opentelemetry.md b/docs/content/1.0.0.Final/extras/opentelemetry.md similarity index 100% rename from docs/content/extras/opentelemetry.md rename to docs/content/1.0.0.Final/extras/opentelemetry.md diff --git a/docs/content/extras/replicated-queue-manager.md b/docs/content/1.0.0.Final/extras/replicated-queue-manager.md similarity index 100% rename from docs/content/extras/replicated-queue-manager.md rename to docs/content/1.0.0.Final/extras/replicated-queue-manager.md diff --git a/docs/content/extras/storage.md b/docs/content/1.0.0.Final/extras/storage.md similarity index 100% rename from docs/content/extras/storage.md rename to docs/content/1.0.0.Final/extras/storage.md diff --git a/docs/content/getting-started.md b/docs/content/1.0.0.Final/getting-started.md similarity index 57% rename from docs/content/getting-started.md rename to docs/content/1.0.0.Final/getting-started.md index a996e4d2d..a1f097fc4 100644 --- a/docs/content/getting-started.md +++ b/docs/content/1.0.0.Final/getting-started.md @@ -25,20 +25,19 @@ Add the reference server dependency to your Maven project: ``` -Then implement an `AgentExecutor` and define an `AgentCard`. See the [Server Guide]({site.url('server')}) for the full walkthrough. +Then implement an `AgentExecutor` and define an `AgentCard`. See the [Server Guide](server) for the full walkthrough. ## Guides | Guide | Description | |-------|-------------| -| [Server Guide]({site.url('server')}) | Run your Java application as an A2A server | -| [Client Guide]({site.url('client')}) | Communicate with A2A-compliant agents | -| [Configuration]({site.url('configuration')}) | Config properties, MicroProfile Config, custom providers | -| [Task Authorization]({site.url('authorization')}) | Per-user access control for multi-user deployments | -| [Backward Compatibility]({site.url('compatibility')}) | Serve v1.0 and v0.3 clients simultaneously | -| [Extras]({site.url('extras')}) | Optional add-ons: database storage, OpenTelemetry, HTTP clients | -| [BOMs]({site.url('boms')}) | Dependency management with Bill of Materials | -| [Examples]({site.url('examples')}) | Hello World walkthroughs and sample applications | +| [Server Guide](server) | Run your Java application as an A2A server | +| [Client Guide](client) | Communicate with A2A-compliant agents | +| [Configuration](configuration) | Config properties, MicroProfile Config, custom providers | +| [Backward Compatibility](compatibility) | Serve v1.0 and v0.3 clients simultaneously | +| [Extras](extras) | Optional add-ons: database storage, OpenTelemetry, HTTP clients | +| [BOMs](boms) | Dependency management with Bill of Materials | +| [Examples](examples) | Hello World walkthroughs and sample applications | ## Requirements diff --git a/docs/content/1.0.0.Final/server.md b/docs/content/1.0.0.Final/server.md new file mode 100644 index 000000000..3a1369519 --- /dev/null +++ b/docs/content/1.0.0.Final/server.md @@ -0,0 +1,166 @@ +--- +title: A2A Server Guide +description: Run your agentic Java application as an A2A server following the Agent2Agent Protocol. +layout: page +--- + +# 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, follow the steps below. + +## Supported Transports + +- JSON-RPC 2.0 +- gRPC +- HTTP+JSON/REST + +## 1. Add a Server Dependency + +### JSON-RPC + +```xml + + org.a2aproject.sdk + a2a-java-sdk-reference-jsonrpc + + $\{org.a2aproject.sdk.version} + +``` + +### gRPC + +```xml + + org.a2aproject.sdk + a2a-java-sdk-reference-grpc + $\{org.a2aproject.sdk.version} + +``` + +### HTTP+JSON/REST + +```xml + + org.a2aproject.sdk + a2a-java-sdk-reference-rest + $\{org.a2aproject.sdk.version} + +``` + +You can add more than one transport dependency to support multiple protocols simultaneously. + +## 2. Define an Agent Card + +```java +@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. Implement an Agent Executor + +```java +@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 { + if (context.getTask() == null) { + agentEmitter.submit(); + } + agentEmitter.startWork(); + + String userMessage = extractTextFromMessage(context.getMessage()); + String response = weatherAgent.chat(userMessage); + + agentEmitter.addArtifact(List.of(new TextPart(response))); + agentEmitter.complete(); + } + + @Override + public void cancel(RequestContext context, AgentEmitter agentEmitter) throws JSONRPCError { + Task task = context.getTask(); + if (task == null) { + agentEmitter.cancel(); + return; + } + if (task.status().state() == TaskState.CANCELED || + task.status().state() == TaskState.COMPLETED) { + throw new TaskNotCancelableError(); + } + agentEmitter.cancel(); + } + + private String extractTextFromMessage(Message message) { + if (message == null) { + return ""; + } + StringBuilder textBuilder = new StringBuilder(); + for (Part part : message.parts()) { + if (part instanceof TextPart textPart) { + textBuilder.append(textPart.text()); + } + } + return textBuilder.toString(); + } + } +} +``` + +## 4. Configuration + +See [Configuration](configuration) for all config properties and tuning. + +## Backward Compatibility with v0.3 + +See [Backward Compatibility](compatibility) for multi-version modules, version routing, and v0.3 client support. + +## Server Integrations + +- **Quarkus** — Reference implementations are Quarkus-based (JSON-RPC, gRPC, REST) +- **Jakarta EE** — [a2a-jakarta](https://github.com/wildfly-extras/a2a-jakarta) works with any Jakarta EE Web Profile runtime + +See [CONTRIBUTING_INTEGRATIONS.md](https://github.com/a2aproject/a2a-java/blob/main/CONTRIBUTING_INTEGRATIONS.md) to submit your own integration. diff --git a/docs/content/authorization.md b/docs/content/1.1.0.Final/authorization.md similarity index 98% rename from docs/content/authorization.md rename to docs/content/1.1.0.Final/authorization.md index 10aa2e6d5..e726cb5ee 100644 --- a/docs/content/authorization.md +++ b/docs/content/1.1.0.Final/authorization.md @@ -33,7 +33,7 @@ public class MyTaskAuthorizationProvider implements TaskAuthorizationProvider { @Override public boolean isTaskRecorded(String taskId) { - return ownershipStore.contains(taskId); + return ownershipStore.containsKey(taskId); } @Override diff --git a/docs/content/1.1.0.Final/boms.md b/docs/content/1.1.0.Final/boms.md new file mode 100644 index 000000000..e0dc6e1b4 --- /dev/null +++ b/docs/content/1.1.0.Final/boms.md @@ -0,0 +1,120 @@ +--- +title: Bill of Materials (BOM) +description: Dependency management BOMs for the A2A Java SDK — SDK, Extras, and Reference BOMs. +layout: page +--- + +# Bill of Materials (BOM) + +The A2A Java SDK provides three BOMs for different use cases, so you can manage dependency versions in one place. + +## BOM Modules + +### SDK BOM + +**Artifact:** `org.a2aproject.sdk:a2a-java-sdk-bom` + +Includes all A2A SDK core modules (spec, server, client, transport), core third-party dependencies, Jakarta APIs, and test utilities. + +**Use this BOM when:** Building A2A agents with any framework (Quarkus, Spring Boot, vanilla Java, etc.) + +### Extras BOM + +**Artifact:** `org.a2aproject.sdk:a2a-java-sdk-extras-bom` + +Includes everything from the SDK BOM plus server-side enhancement modules (database persistence, distributed queue management, etc.). + +**Use this BOM when:** Building production A2A servers needing advanced server-side features beyond the core SDK. + +### Reference BOM + +**Artifact:** `org.a2aproject.sdk:a2a-java-sdk-reference-bom` + +Includes everything from the SDK BOM plus the Quarkus BOM (complete Quarkus platform), A2A reference implementation modules, and the TCK module for testing. + +**Use this BOM when:** Building Quarkus-based A2A agents or reference implementations. + +## Usage + +### SDK BOM (Any Framework) + +```xml + + + + org.a2aproject.sdk + a2a-java-sdk-bom + $\{org.a2aproject.sdk.version} + pom + import + + + + + + + + org.a2aproject.sdk + a2a-java-sdk-server-common + + + org.a2aproject.sdk + a2a-java-sdk-transport-jsonrpc + + +``` + +### Extras BOM (Database Persistence, Distributed Deployments) + +```xml + + + + org.a2aproject.sdk + a2a-java-sdk-extras-bom + $\{org.a2aproject.sdk.version} + pom + import + + + + + + + org.a2aproject.sdk + a2a-java-sdk-server-common + + + org.a2aproject.sdk + a2a-java-extras-task-store-database-jpa + + +``` + +### Reference BOM (Quarkus) + +```xml + + + + org.a2aproject.sdk + a2a-java-sdk-reference-bom + $\{org.a2aproject.sdk.version} + pom + import + + + + + + + + org.a2aproject.sdk + a2a-java-sdk-reference-jsonrpc + + + io.quarkus + quarkus-arc + + +``` diff --git a/docs/content/1.1.0.Final/client.md b/docs/content/1.1.0.Final/client.md new file mode 100644 index 000000000..c39bb24f9 --- /dev/null +++ b/docs/content/1.1.0.Final/client.md @@ -0,0 +1,197 @@ +--- +title: A2A Client Guide +description: Communicate with A2A-compliant agents using the A2A Java SDK client. +layout: page +--- + +# A2A Client + +The A2A Java SDK provides a Java client for communicating with any A2A-compliant agent. Supports JSON-RPC 2.0, gRPC, and HTTP+JSON/REST transports. + +## 1. Add the Client Dependency + +```xml + + org.a2aproject.sdk + a2a-java-sdk-client + + $\{org.a2aproject.sdk.version} + +``` + +The client artifact includes the JSON-RPC transport by default. For gRPC or REST, add the corresponding transport: + +```xml + + + org.a2aproject.sdk + a2a-java-sdk-client-transport-grpc + $\{org.a2aproject.sdk.version} + + + + + org.a2aproject.sdk + a2a-java-sdk-client-transport-rest + $\{org.a2aproject.sdk.version} + +``` + +## 2. Create a Client + +```java +// Resolve the agent card from the server +AgentCard agentCard = A2ACardResolver.builder() + .baseUrl("http://localhost:1234") + .build() + .getAgentCard(); + +// Configure accepted output modes +ClientConfig clientConfig = new ClientConfig.Builder() + .setAcceptedOutputModes(List.of("text")) + .build(); + +// Define event consumers +List> consumers = List.of( + (event, card) -> { + if (event instanceof MessageEvent messageEvent) { + // handle message + } else if (event instanceof TaskEvent taskEvent) { + // handle task + } else if (event instanceof TaskUpdateEvent updateEvent) { + // handle task update + } + } +); + +// Build the client +Client client = Client + .builder(agentCard) + .clientConfig(clientConfig) + .withTransport(JSONRPCTransport.class, new JSONRPCTransportConfig()) + .addConsumers(consumers) + .streamingErrorHandler(error -> { /* handle errors */ }) + .build(); +``` + +## 3. Send Messages + +```java +// Send a text message (streaming used automatically if supported) +Message message = A2A.toUserMessage("tell me a joke"); +client.sendMessage(message); + +// Send with per-call custom consumers +client.sendMessage(message, customConsumers, customErrorHandler); + +// Send with a call context +client.sendMessage(message, clientCallContext); +``` + +## Task Management + +```java +// Get task state +Task task = client.getTask(new TaskQueryParams("task-1234")); +Task task = client.getTask(new TaskQueryParams("task-1234", 10)); // with history limit + +// Cancel a task +Task cancelled = client.cancelTask(new TaskIdParams("task-1234")); + +// Subscribe to an ongoing task +client.subscribeToTask(new TaskIdParams("task-1234")); +client.subscribeToTask(taskIdParams, customConsumers, customErrorHandler); + +// Retrieve the server agent card +AgentCard serverCard = client.getAgentCard(); +``` + +## Push Notifications + +```java +// Set a push notification configuration +PushNotificationConfig pushConfig = PushNotificationConfig.builder() + .url("https://example.com/callback") + .authenticationInfo(new AuthenticationInfo(List.of("jwt"), null)) + .build(); + +TaskPushNotificationConfig taskConfig = TaskPushNotificationConfig.builder() + .taskId("task-1234") + .pushNotificationConfig(pushConfig) + .build(); + +client.createTaskPushNotificationConfiguration(taskConfig); + +// Get a specific configuration +TaskPushNotificationConfig config = client.getTaskPushNotificationConfiguration( + new GetTaskPushNotificationConfigParams("task-1234", "config-4567")); + +// List configurations +List configs = + client.listTaskPushNotificationConfigurations( + new ListTaskPushNotificationConfigParams("task-1234")); + +// Delete a configuration +client.deleteTaskPushNotificationConfigurations( + new DeleteTaskPushNotificationConfigParams("task-1234", "config-4567")); +``` + +## Transport Configuration + +### JSON-RPC with a Custom HTTP Client + +```java +// Use a custom JDK HTTP client +HttpClient jdkHttpClient = HttpClient.newBuilder() + .connectTimeout(Duration.ofSeconds(5)) + .followRedirects(HttpClient.Redirect.NORMAL) + .build(); + +Client client = Client + .builder(agentCard) + .withTransport(JSONRPCTransport.class, + new JSONRPCTransportConfig(new JdkA2AHttpClient(jdkHttpClient))) + .build(); +``` + +### REST with a Custom HTTP Client + +```java +A2AHttpClient customHttpClient = ... + +Client client = Client + .builder(agentCard) + .withTransport(RestTransport.class, new RestTransportConfig(customHttpClient)) + .build(); +``` + +### gRPC + +```java +Function channelFactory = agentUrl -> + ManagedChannelBuilder.forTarget(agentUrl).build(); + +Client client = Client + .builder(agentCard) + .withTransport(GrpcTransport.class, new GrpcTransportConfig(channelFactory)) + .build(); +``` + +### Multiple Transports + +```java +Client client = Client + .builder(agentCard) + .withTransport(GrpcTransport.class, new GrpcTransportConfig(channelFactory)) + .withTransport(JSONRPCTransport.class, new JSONRPCTransportConfig()) + .withTransport(RestTransport.class, new RestTransportConfig()) + .build(); +``` + +## Communicating with v0.3 Agents + +See [Backward Compatibility](compatibility#client-communicating-with-v03-agents) for using `Client_v0_3` with older protocol agents. + +## Examples + +See [Examples](examples) for Hello World walkthroughs and sample applications. diff --git a/docs/content/1.1.0.Final/compatibility.md b/docs/content/1.1.0.Final/compatibility.md new file mode 100644 index 000000000..aae404e3e --- /dev/null +++ b/docs/content/1.1.0.Final/compatibility.md @@ -0,0 +1,124 @@ +--- +title: Backward Compatibility +description: Serve v1.0 and v0.3 A2A protocol versions simultaneously — multi-version modules, version routing, and v0.3 client support. +layout: page +--- + +# Backward Compatibility with v0.3 + +Add compat modules alongside v1.0 modules to serve both protocol versions simultaneously. No changes to your `AgentExecutor` are needed. + +## Server: Multi-Version Module (recommended) + +```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} + +``` + +## Server: 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} + +``` + +## 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. + +## Client: Communicating with v0.3 Agents + +Use `Client_v0_3` to communicate with agents that only support protocol v0.3: + +```xml + + org.a2aproject.sdk + a2a-java-sdk-compat-0.3-client + $\{org.a2aproject.sdk.version} + + + org.a2aproject.sdk + a2a-java-sdk-compat-0.3-client-transport-jsonrpc + $\{org.a2aproject.sdk.version} + +``` + +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(); + +AgentInterface v03Interface = card.supportedInterfaces().stream() + .filter(i -> A2AProtocol_v0_3.PROTOCOL_VERSION.equals(i.protocolVersion())) + .findFirst().orElseThrow(); + +Client_v0_3 client = ClientBuilder_v0_3.forUrl(v03Interface.url()) + .withTransport(JSONRPCTransport_v0_3.class, new JSONRPCTransportConfigBuilder_v0_3()) + .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. diff --git a/docs/content/1.1.0.Final/configuration.md b/docs/content/1.1.0.Final/configuration.md new file mode 100644 index 000000000..b164bf3cf --- /dev/null +++ b/docs/content/1.1.0.Final/configuration.md @@ -0,0 +1,150 @@ +--- +title: Configuration +description: Configure the A2A Java SDK — properties, MicroProfile Config integration, custom providers. +layout: page +--- + +# Configuration + +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` integration to override defaults via `application.properties`, environment variables, or system properties +- **Spring/other frameworks**: Implement a custom `A2AConfigProvider` (see [Custom Config Providers](#custom-config-providers) below) +- **Reference implementations**: Already include the MicroProfile Config integration + +## Configuration Properties + +### Executor Settings + +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 + +```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 +``` + +### Tuning Guidelines + +- **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. + +## MicroProfile Config Integration + +Add the integration dependency to override configuration via standard MicroProfile Config sources: + +```xml + + org.a2aproject.sdk + a2a-java-sdk-microprofile-config + $\{org.a2aproject.sdk.version} + +``` + +Once added, you can set any A2A property through: + +**application.properties:** +```properties +a2a.executor.core-pool-size=10 +a2a.executor.max-pool-size=100 +a2a.blocking.agent.timeout.seconds=60 +``` + +**Environment variables:** +```bash +export A2A_EXECUTOR_CORE_POOL_SIZE=10 +export A2A_BLOCKING_AGENT_TIMEOUT_SECONDS=60 +``` + +**System properties:** +```bash +java -Da2a.executor.core-pool-size=10 -jar your-app.jar +``` + +### Configuration Fallback Chain + +``` +MicroProfile Config Sources (application.properties, env vars, -D flags) + | (not found?) +DefaultValuesConfigProvider + -> Scans classpath for ALL META-INF/a2a-defaults.properties files + -> Merges all discovered properties together + -> Throws exception if duplicate keys found + | (property exists?) +Return merged default value + | (not found?) +IllegalArgumentException +``` + +All `META-INF/a2a-defaults.properties` files (from server-common, extras modules, etc.) are loaded and merged together by `DefaultValuesConfigProvider` at startup. This is not a sequential fallback chain, but a single merged set of defaults. + +### Framework Compatibility + +The MicroProfile Config integration works with any MicroProfile Config implementation: + +- **Quarkus** -- Built-in MicroProfile Config support +- **Helidon** -- Built-in MicroProfile Config support +- **Open Liberty** -- Built-in MicroProfile Config support +- **WildFly/JBoss EAP** -- Add `smallrye-config` dependency +- **Other Jakarta EE servers** -- Add MicroProfile Config implementation + +## Custom Config Providers + +If you're using a different framework (Spring, Micronaut, etc.), implement your own `A2AConfigProvider`: + +```java +@ApplicationScoped +@Alternative +@Priority(100) // Higher than MicroProfileConfigProvider's priority of 50 +public class MyConfigProvider implements A2AConfigProvider { + + private final Properties customConfig; + + @Inject + DefaultValuesConfigProvider defaultValues; + + MyConfigProvider() { + customConfig = loadFromYourFramework(); + } + + @Override + public String getValue(String name) { + String value = customConfig.getProperty(name); + if (value != null) { + return value; + } + return defaultValues.getValue(name); + } + + @Override + public Optional getOptionalValue(String name) { + String value = customConfig.getProperty(name); + if (value != null) { + return Optional.of(value); + } + return defaultValues.getOptionalValue(name); + } +} +``` + +**Note:** The reference server implementations (Quarkus-based) automatically include the MicroProfile Config integration, so properties work out of the box in `application.properties`. diff --git a/docs/content/1.1.0.Final/examples.md b/docs/content/1.1.0.Final/examples.md new file mode 100644 index 000000000..9d5b0fb40 --- /dev/null +++ b/docs/content/1.1.0.Final/examples.md @@ -0,0 +1,149 @@ +--- +title: Examples +description: Hello World examples for the A2A Java SDK — server and client walkthroughs with transport selection and OpenTelemetry. +layout: page +--- + +# Examples + +## Prerequisites + +- Java 17 or higher +- Python 3.8 or higher +- [uv](https://github.com/astral-sh/uv) (recommended Python package installer) +- Git + +## Hello World Server + +This example runs a Java A2A server that a Python client can talk to. + +### Start the Java Server + +```bash +cd examples/helloworld/server +mvn quarkus:dev +``` + +### Transport Protocol Selection + +Select the transport protocol via the `quarkus.agentcard.protocol` property: + +```bash +# JSON-RPC (default) +mvn quarkus:dev + +# gRPC +mvn quarkus:dev -Dquarkus.agentcard.protocol=GRPC + +# HTTP+JSON/REST +mvn quarkus:dev -Dquarkus.agentcard.protocol=HTTP+JSON +``` + +You can also set the default in `src/main/resources/application.properties`: +```properties +quarkus.agentcard.protocol=HTTP+JSON +``` + +### Run the Python Client + +The Python client is part of the [a2a-samples](https://github.com/a2aproject/a2a-samples) project: + +```bash +git clone https://github.com/a2aproject/a2a-samples.git +cd a2a-samples/samples/python/agents/helloworld + +uv venv +source .venv/bin/activate +uv pip install -e . +uv run test_client.py +``` + +The client connects to `http://localhost:9999`, fetches the agent card, sends a regular message, then sends the same message as a streaming request. + +## Hello World Client + +This example runs a Java A2A client that talks to a Python server. + +### Start the Python Server + +```bash +git clone https://github.com/a2aproject/a2a-samples.git +cd a2a-samples/samples/python/agents/helloworld + +uv venv +source .venv/bin/activate +uv pip install -e . +uv run . +``` + +The server starts on `http://localhost:9999`. You can also use the [Java server example](#hello-world-server) instead. + +### Run the Java Client + +First build the SDK, then run the client: + +```bash +# From the a2a-java root +mvn clean install + +# Run the client +cd examples/helloworld/client +mvn exec:java +``` + +#### Transport Protocol Selection + +```bash +# JSON-RPC (default) +mvn exec:java + +# gRPC +mvn exec:java -Dquarkus.agentcard.protocol=GRPC + +# HTTP+JSON/REST +mvn exec:java -Dquarkus.agentcard.protocol=HTTP+JSON +``` + +The protocol you select on the client must match the protocol configured on the server. + +#### Using JBang + +Alternatively, run the client with [JBang](https://www.jbang.dev/) (no Maven required): + +```bash +jbang examples/helloworld/client/src/main/java/org/a2aproject/sdk/examples/helloworld/HelloWorldRunner.java +``` + +Pass transport and OpenTelemetry flags the same way: +```bash +jbang examples/helloworld/client/src/main/java/org/a2aproject/sdk/examples/helloworld/HelloWorldRunner.java \ + -Dquarkus.agentcard.protocol=GRPC -Dopentelemetry=true +``` + +## OpenTelemetry (Optional) + +Both the server and client support distributed tracing with OpenTelemetry. + +### Server with OpenTelemetry + +```bash +cd examples/helloworld/server +mvn quarkus:dev -Popentelemetry +``` + +Quarkus Dev Services automatically starts a Grafana observability stack. Open Grafana at `http://localhost:3001` (credentials: admin/admin) and view traces in the "Explore" section using the Tempo data source. + +### Client with OpenTelemetry + +```bash +cd examples/helloworld/client +mvn exec:java -Dopentelemetry=true +``` + +The client expects an OpenTelemetry collector on port 5317. The easiest way is to run the Java server with `-Popentelemetry` (which starts the collector automatically), then run the client with `-Dopentelemetry=true` for end-to-end traces. + +For more information, see the [OpenTelemetry extras module](extras/opentelemetry). + +## More Examples + +- [a2a-samples repository](https://github.com/a2aproject/a2a-samples/tree/main/samples/java/agents) — Additional agent examples in Java and other languages diff --git a/docs/content/1.1.0.Final/extras.md b/docs/content/1.1.0.Final/extras.md new file mode 100644 index 000000000..2a4478b5e --- /dev/null +++ b/docs/content/1.1.0.Final/extras.md @@ -0,0 +1,45 @@ +--- +title: Extras +description: Optional add-ons for the A2A Java SDK including alternative HTTP clients, database-backed stores, distributed queue replication, and OpenTelemetry observability. +layout: page +--- + +# Extras + +The A2A Java SDK ships with sensible defaults — an in-memory task store, an in-memory push notification config store, a single-instance queue manager, and a JDK HttpClient. The **extras** modules are drop-in replacements that swap these defaults for production-grade alternatives. + +All extras use **Java SPI** or **CDI alternative** discovery, so adding the dependency is typically all you need — no code changes required. + +## BOM + +Import the extras BOM to manage versions: + +```xml + + + + org.a2aproject.sdk + a2a-java-extras-bom + $\{org.a2aproject.sdk.version} + pom + import + + + +``` + +## [HTTP Clients](extras/http-clients) + +Alternative `A2AHttpClient` implementations. The **Vert.x HTTP Client** (`a2a-java-sdk-http-client-vertx`) replaces the default JDK HttpClient with a reactive, non-blocking Vert.x WebClient — recommended for Quarkus, reactive frameworks, and high-throughput scenarios. An **Android HTTP Client** (`a2a-java-sdk-http-client-android`) is also available for mobile applications. + +## [Storage & Persistence](extras/storage) + +JPA-backed replacements for the in-memory stores, providing database persistence for production and load-balanced deployments. Includes a **JPA Task Store** (`a2a-java-extras-task-store-database-jpa`) and a **JPA Push Notification Config Store** (`a2a-java-extras-push-notification-config-store-database-jpa`). Both use Jakarta Persistence API (JPA 3.0+) and share the same datasource configuration. + +## [Replicated Queue Manager](extras/replicated-queue-manager) + +Replaces the default `InMemoryQueueManager` with event replication across multiple A2A server instances via message brokers. Required for multi-instance deployments. The core module (`a2a-java-queue-manager-replicated-core`) pairs with a MicroProfile Reactive Messaging strategy (`a2a-java-queue-manager-replication-mp-reactive`) supporting Apache Kafka, Pulsar, or AMQP. You can also write your own `ReplicationStrategy`. + +## [OpenTelemetry](extras/opentelemetry) + +Distributed tracing, metrics, and context propagation for A2A servers and clients using OpenTelemetry. The server module (`a2a-java-sdk-opentelemetry-server`) adds automatic span creation for all protocol methods with context propagation across async boundaries. Client modules (`a2a-java-sdk-opentelemetry-client`, `a2a-java-sdk-opentelemetry-client-propagation`) instrument A2A client operations. diff --git a/docs/content/1.1.0.Final/extras/http-clients.md b/docs/content/1.1.0.Final/extras/http-clients.md new file mode 100644 index 000000000..c47050c08 --- /dev/null +++ b/docs/content/1.1.0.Final/extras/http-clients.md @@ -0,0 +1,207 @@ +--- +title: HTTP Clients +description: Alternative HTTP client implementations for the A2A Java SDK, including Vert.x WebClient for reactive applications and an Android-specific client. +layout: page +--- + +# HTTP Clients + +The A2A SDK uses an `A2AHttpClient` abstraction for all HTTP communication — fetching agent cards, making transport calls, and Server-Sent Events (SSE) streaming. By default, the SDK uses a JDK 11+ HttpClient implementation. These extras provide alternative implementations. + +## Vert.x HTTP Client + +Replaces the default `JdkA2AHttpClient` with a [Vert.x WebClient](https://vertx.io/docs/vertx-web-client/java/)-based implementation (`VertxA2AHttpClient`) for reactive, non-blocking HTTP communication. + +### When to Use + +**Recommended for:** + +- Quarkus applications (Vert.x is already included) +- Reactive applications using Vert.x or reactive frameworks +- High-throughput scenarios with many concurrent requests +- Applications requiring efficient SSE streaming +- Cloud-native deployments optimizing for resource usage + +**Not needed for** simple, low-volume applications or environments where JDK HttpClient performs adequately. + +### What It Solves + +| | JDK HttpClient | Vert.x WebClient | +|--|----------------|-------------------| +| **I/O model** | Platform threads, blocking | Event loop, non-blocking | +| **Memory** | Higher (one thread per connection) | Lower (shared event loop) | +| **HTTP/2** | Manual configuration | Automatic ALPN negotiation | +| **Reactive integration** | Requires bridging | Native | + +### 1. Add Dependency + +```xml + + org.a2aproject.sdk + a2a-java-sdk-http-client-vertx + +``` + +For non-Quarkus environments, also add `vertx-web-client`: + +```xml + + io.vertx + vertx-web-client + +``` + +### 2. Automatic Discovery (No Code Changes) + +The `VertxA2AHttpClientProvider` has **priority 100** (vs. 0 for the JDK client). The SDK's `A2AHttpClientFactory` uses `ServiceLoader` to discover and select the highest-priority provider available: + +```java +// No changes needed — A2A SDK automatically uses VertxA2AHttpClient +A2ACardResolver resolver = A2ACardResolver.builder().baseUrl("http://localhost:9999").build(); +AgentCard card = resolver.getAgentCard(); // Uses Vert.x under the hood + +Client client = Client.builder(card) + .withTransport(JSONRPCTransport.class, new JSONRPCTransportConfig()) + .build(); +``` + +If Vert.x classes are not on the classpath, the provider returns priority `-1` and the SDK falls back to the JDK HttpClient. + +### Usage Examples + +#### GET Request + +```java +try (A2AHttpClient client = A2AHttpClientFactory.create()) { + A2AHttpResponse response = client.createGet() + .url("https://api.example.com/data") + .addHeader("Authorization", "Bearer token") + .get(); + + if (response.success()) { + System.out.println(response.body()); + } +} +``` + +#### POST Request with JSON Body + +```java +try (A2AHttpClient client = A2AHttpClientFactory.create()) { + A2AHttpResponse response = client.createPost() + .url("https://api.example.com/submit") + .addHeader("Content-Type", "application/json") + .body("{\"key\":\"value\"}") + .post(); + + System.out.println("Status: " + response.status()); +} +``` + +#### Server-Sent Events (SSE) Streaming + +```java +try (A2AHttpClient client = A2AHttpClientFactory.create()) { + CompletableFuture future = client.createGet() + .url("https://api.example.com/stream") + .getAsyncSSE( + message -> System.out.println("Received: " + message), + error -> error.printStackTrace(), + () -> System.out.println("Stream complete") + ); + + // Do other work while streaming... + future.join(); +} +``` + +### CDI / Quarkus Integration + +In Quarkus or other CDI environments, the `VertxA2AHttpClient` automatically discovers and reuses the CDI-managed `Vertx` instance so that your entire application shares the same event loop. + +For non-CDI environments, you can share a Vert.x instance manually: + +```java +Vertx vertx = Vertx.vertx(); + +try { + try (VertxA2AHttpClient client = new VertxA2AHttpClient(vertx)) { + A2AHttpResponse response = client.createGet() + .url("https://example.com") + .get(); + } +} finally { + vertx.close(); +} +``` + +### Lifecycle Management + +| Usage | Behavior on close | +|-------|--------------------| +| Standalone (`new VertxA2AHttpClient()`) | Both `WebClient` and `Vertx` are closed | +| CDI/Quarkus (`new VertxA2AHttpClient(injectedVertx)`) | Only `WebClient` is closed; `Vertx` remains open | + +Always use try-with-resources to avoid leaking `Vertx` instances. + +### Thread Safety + +- **Client instances** are thread-safe — multiple threads can use the same client +- **Builder instances** are NOT thread-safe — create separate builders per thread +- All I/O operations execute on Vert.x event loop threads + +### Performance Notes + +Synchronous methods (`.get()`, `.post()`, `.delete()`) block the calling thread despite using Vert.x's async API internally. Vert.x still provides benefits at the network layer: non-blocking I/O, efficient connection pooling, and lower thread usage overall. + +Async methods (`.getAsyncSSE()`, `.postAsyncSSE()`) are truly async — they return a `CompletableFuture` immediately. + +### Troubleshooting + +**Client not being used** — If logs show `JdkA2AHttpClient` instead of `VertxA2AHttpClient`, verify Vert.x WebClient is on the classpath: + +```bash +mvn dependency:tree | grep vertx-web-client +``` + +**ClassNotFoundException for Vert.x** — Add the `vertx-web-client` dependency (see above). For Quarkus, ensure `quarkus-vertx` is present. + +**Memory leaks** — Always use try-with-resources when creating `VertxA2AHttpClient` with the no-args constructor. + +### Version Compatibility + +- **Java**: 17+ +- **Vert.x**: 4.x (tested with 4.5.0+) +- **Quarkus**: Any version using Vert.x 4.x +- **Jakarta EE**: 9.0+ + +## Android HTTP Client + + +HTTP client implementation for Android applications, using `HttpURLConnection` under the hood — no external dependencies required. + +### When to Use + +- Android applications targeting API level 24+ (Android 7.0+) +- Mobile apps that need to communicate with A2A agents + +### 1. Add Dependency + +```xml + + org.a2aproject.sdk + a2a-java-sdk-http-client-android + +``` + + +### 2. Automatic Discovery (No Code Changes) + +The `AndroidA2AHttpClientProvider` has **priority 110** and detects the Android runtime automatically. On non-Android runtimes, the provider returns priority `-1` and the SDK falls back to the next available client. + +### Features + +- **GET, POST, DELETE** requests with fluent builder API +- **SSE streaming** via `getAsyncSSE()` and `postAsyncSSE()` — async operations run on a dedicated cached thread pool (`A2A-Android-Net`) +- **Timeouts**: 15 s connection, 60 s read +- **Response size limit**: 10 MB diff --git a/docs/content/1.1.0.Final/extras/opentelemetry.md b/docs/content/1.1.0.Final/extras/opentelemetry.md new file mode 100644 index 000000000..7b6dcb68c --- /dev/null +++ b/docs/content/1.1.0.Final/extras/opentelemetry.md @@ -0,0 +1,189 @@ +--- +title: OpenTelemetry +description: OpenTelemetry observability integration for the A2A Java SDK with distributed tracing, context propagation, and metrics for servers and clients. +layout: page +--- + +# OpenTelemetry + +Adds distributed tracing, metrics, and context propagation to A2A servers and clients using [OpenTelemetry](https://opentelemetry.io/). + +## Features + +- **Distributed Tracing**: Automatic span creation for all A2A protocol methods +- **Context Propagation**: OpenTelemetry trace context propagation across async operations +- **Request/Response Logging**: Optional extraction of request and response data into spans +- **Error Tracking**: Automatic error status and error type attributes on failures + +## Modules + +| Module | Artifact ID | Description | +|--------|-------------|-------------| +| Common | `a2a-java-sdk-opentelemetry-common` | Shared utilities and constants | +| Server | `a2a-java-sdk-opentelemetry-server` | Server-side tracing | +| Client | `a2a-java-sdk-opentelemetry-client` | Client-side instrumentation | +| Client Propagation | `a2a-java-sdk-opentelemetry-client-propagation` | Context propagation for async client operations | + +## Server + +The `OpenTelemetryRequestHandlerDecorator` wraps the default request handler to create spans for every A2A protocol method, with automatic error tracking. + +### 1. Add Dependency + +```xml + + org.a2aproject.sdk + a2a-java-sdk-opentelemetry-server + +``` + +### Architecture + +``` +Client Request + | +OpenTelemetryRequestHandlerDecorator + | (creates span) +Default RequestHandler + | +Agent Execution (with context propagation) + | +Response +``` + +``` +HTTP Request (with trace headers) + | +OpenTelemetry extracts context + | +Span created for A2A method + | +ManagedExecutor propagates context + | +Async agent execution (maintains trace context) + | +Response (with trace headers) +``` + +### Context-Aware Async Executor + +> **Note:** The `AsyncManagedExecutorProducer` is provided by the **Quarkus reference server** ([`reference/common`](https://github.com/a2aproject/a2a-java/blob/main/reference/common/src/main/java/org/a2aproject/sdk/server/common/quarkus/AsyncManagedExecutorProducer.java)), not the OpenTelemetry module. It is documented here because it enables context propagation (including trace context) across async boundaries. + + +The reference server replaces the default `AsyncExecutorProducer` with `AsyncManagedExecutorProducer`: + +- **Priority 20**: takes precedence over the default executor (priority 10) +- **Automatic activation**: no configuration needed — included automatically in the Quarkus reference server +- **Context propagation**: uses MicroProfile Context Propagation to maintain trace context and CDI request context across async boundaries + +> **Note:** Unlike the default `AsyncExecutorProducer`, the `AsyncManagedExecutorProducer` does not use the `a2a.executor.*` configuration properties. Pool sizing is controlled by the container's `ManagedExecutor` configuration. + +#### Quarkus Configuration + +```properties +quarkus.thread-pool.core-threads=10 +quarkus.thread-pool.max-threads=50 +quarkus.thread-pool.queue-size=100 +``` + +For other runtimes, consult your MicroProfile Context Propagation implementation documentation. + +### Span Attributes + +The following attributes are automatically added to spans: + +| Attribute | Description | +|-----------|-------------| +| `gen_ai.agent.a2a.request` | Request parameters (if extraction enabled) | +| `gen_ai.agent.a2a.response` | Response data (if extraction enabled) | +| `error.type` | Error message (on failures) | + +### Request/Response Extraction + +Enable request and response data extraction in spans using JVM system properties: + +``` +# Extract request parameters into span attributes (disabled by default) +-Dorg.a2aproject.sdk.server.extract.request=true + +# Extract response data into span attributes (disabled by default) +-Dorg.a2aproject.sdk.server.extract.response=true +``` + +> **Warning:** Extracting request/response data may expose sensitive information in traces. Use with caution in production environments. + +## Client + +### Instrumentation + +Adds OpenTelemetry spans to A2A client operations: + +```xml + + org.a2aproject.sdk + a2a-java-sdk-opentelemetry-client + +``` + +### Context Propagation + +For trace context propagation across async client boundaries: + +```xml + + org.a2aproject.sdk + a2a-java-sdk-opentelemetry-client-propagation + +``` + +## Troubleshooting + +### Context Not Propagating + +**Symptom:** Spans in async tasks are not linked to parent spans. + +**Solution:** Ensure the OpenTelemetry server module is included and check logs for: + +``` +Initializing ManagedExecutor for async operations with CDI context propagation +``` + +### ManagedExecutor Not Available + +**Symptom:** `IllegalStateException: ManagedExecutor not injected - ensure MicroProfile Context Propagation is available` + +**Solution:** Ensure your runtime provides MicroProfile Context Propagation. For Quarkus, add: + +```xml + + io.quarkus + quarkus-smallrye-context-propagation + +``` + +### Performance Impact + +- Disable request/response extraction in production unless needed +- Configure sampling rate to reduce trace volume +- Ensure your OpenTelemetry collector is properly sized + +## Best Practices + +1. Configure appropriate **sampling rates** for production environments +2. Disable request/response extraction if handling **sensitive data** +3. Add **service name and version** as resource attributes +4. Use **batch processors** in your OpenTelemetry collector to reduce network overhead +5. Monitor the OpenTelemetry **collector's health** and performance + +## Dependencies + +- MicroProfile Telemetry 2.0.1+ +- MicroProfile Context Propagation 1.3+ +- OpenTelemetry API +- A2A Server Common + +## See Also + +- [OpenTelemetry Documentation](https://opentelemetry.io/docs/) +- [MicroProfile Telemetry Specification](https://github.com/eclipse/microprofile-telemetry) +- [MicroProfile Context Propagation](https://github.com/eclipse/microprofile-context-propagation) diff --git a/docs/content/1.1.0.Final/extras/replicated-queue-manager.md b/docs/content/1.1.0.Final/extras/replicated-queue-manager.md new file mode 100644 index 000000000..2d650bd93 --- /dev/null +++ b/docs/content/1.1.0.Final/extras/replicated-queue-manager.md @@ -0,0 +1,288 @@ +--- +title: Replicated Queue Manager +description: Distributed QueueManager implementation for the A2A Java SDK that replicates events across multiple server instances using message brokers like Apache Kafka. +layout: page +--- + +# Replicated Queue Manager + +Replaces the default `InMemoryQueueManager` with a queue manager that replicates events across multiple A2A server instances. Required for multi-instance (load-balanced) deployments where all nodes must see every event. + +## Architecture + +The replicated queue manager has two core components: + +- **`ReplicatedQueueManager`** — wraps the default `InMemoryQueueManager` and handles event replication. It is annotated to take precedence over the default, making it a drop-in replacement. +- **`ReplicationStrategy`** — interface for pluggable replication implementations. A `ReplicationStrategy` **must** be provided when using `ReplicatedQueueManager`. + +The provided implementation, `ReactiveMessagingReplicationStrategy`, uses MicroProfile Reactive Messaging with message brokers like Apache Kafka. + +## Event Flow + +1. An event is generated in the A2A system (e.g., `TaskStatusUpdateEvent`) and enqueued locally +2. A `ReplicationHook` intercepts the event and sends it to the replication strategy +3. The replication strategy serializes the event and sends it to the message broker +4. Other A2A instances receive the event from the broker +5. The received event is deserialized and enqueued in each instance's local queue + +### Replicated Event Types + +The system replicates these event types while preserving their specific types: + +- `TaskStatusUpdateEvent` — task state changes (SUBMITTED, COMPLETED, etc.) +- `TaskArtifactUpdateEvent` — task artifact changes +- `Message` — chat messages and responses +- `Task` — complete task objects +- `A2AError` — error events + +Events are serialized using Jackson with polymorphic type information: + +```json +{ + "taskId": "task-123", + "event": { + "@type": "TaskStatusUpdateEvent", + "taskId": "task-123", + "status": { + "state": "completed", + "timestamp": "2023-09-29T10:30:00Z" + }, + "final": true, + "kind": "status-update" + } +} +``` + +## Quick Start + +### 1. Add Dependencies + +```xml + + + org.a2aproject.sdk + a2a-java-queue-manager-replicated-core + + + + + org.a2aproject.sdk + a2a-java-queue-manager-replication-mp-reactive + +``` + +### 2. Configure Kafka + +Add to your `application.properties`: + +```properties +# Outgoing (QueueManager -> Kafka) +mp.messaging.outgoing.replicated-events-out.connector=smallrye-kafka +mp.messaging.outgoing.replicated-events-out.topic=replicated-events +mp.messaging.outgoing.replicated-events-out.value.serializer=org.apache.kafka.common.serialization.StringSerializer + +# Incoming (Kafka -> QueueManager) +mp.messaging.incoming.replicated-events-in.connector=smallrye-kafka +mp.messaging.incoming.replicated-events-in.topic=replicated-events +mp.messaging.incoming.replicated-events-in.value.deserializer=org.apache.kafka.common.serialization.StringDeserializer +``` + +The channel names `replicated-events-in` and `replicated-events-out` correspond to the `@Incoming` and `@Channel` annotations in the `ReactiveMessagingReplicationStrategy`. + +### 3. Create the Kafka Topic + +```bash +kafka-topics.sh --create --topic replicated-events \ + --bootstrap-server localhost:9092 \ + --partitions 3 --replication-factor 1 +``` + +## Configuration + +### Kafka Settings + +```properties +# Kafka broker configuration +kafka.bootstrap.servers=kafka-broker-1:9092,kafka-broker-2:9092 + +# Topic configuration +mp.messaging.outgoing.replicated-events-out.topic=my-replicated-events +mp.messaging.incoming.replicated-events-in.topic=my-replicated-events + +# Consumer group (all A2A instances should share the same group) +mp.messaging.incoming.replicated-events-in.group.id=a2a-instance-group + +# Consumer behavior +mp.messaging.incoming.replicated-events-in.auto.offset.reset=earliest + +# Reliability configuration +mp.messaging.outgoing.replicated-events-out.acks=all +mp.messaging.outgoing.replicated-events-out.retries=3 + +# Performance tuning +mp.messaging.outgoing.replicated-events-out.batch.size=16384 +mp.messaging.incoming.replicated-events-in.max.poll.records=500 + +# Serialization (key serialization needed for partition-by-taskId strategy) +mp.messaging.outgoing.replicated-events-out.key.serializer=org.apache.kafka.common.serialization.StringSerializer +mp.messaging.incoming.replicated-events-in.key.deserializer=org.apache.kafka.common.serialization.StringDeserializer +``` + +## Production Considerations + +### Kafka Partitioning Strategy + +How you partition your Kafka topic significantly impacts scalability and correctness. + +#### Single Partition + +```bash +kafka-topics.sh --create --topic replicated-events \ + --bootstrap-server localhost:9092 \ + --partitions 1 --replication-factor 1 +``` + +- Guarantees global event ordering +- Suitable for development, testing, and low-throughput production (<1000 events/sec) +- Cannot parallelize consumption + +#### Partition by Task ID (Recommended) + +```bash +kafka-topics.sh --create --topic replicated-events \ + --bootstrap-server localhost:9092 \ + --partitions 10 --replication-factor 3 +``` + +The `ReactiveMessagingReplicationStrategy` sends the `taskId` as the Kafka message key, so Kafka automatically partitions by task ID. + +- **Per-task ordering guarantee**: all events for a single task go to the same partition +- **Horizontal scalability**: different tasks can be processed in parallel across partitions +- **Consumer parallelism**: multiple consumer instances can process different partitions concurrently +- **Rule of thumb**: number of partitions >= number of A2A instances + +### Consumer Group Configuration + +When using multiple partitions, ensure all A2A instances belong to the same consumer group: + +```properties +mp.messaging.incoming.replicated-events-in.group.id=a2a-instance-group +``` + +This ensures each partition is consumed by exactly one instance, and events for the same task always reach the same instance. + +### Poison Pill Cleanup + +When a task reaches a final state (COMPLETED, FAILED, CANCELED), all nodes must terminate their event consumers for that task. This is handled automatically via a `QueueClosedEvent` (poison pill) that is replicated to all nodes. + +The mechanism uses **transaction-aware CDI events**: the poison pill is only sent AFTER the final task state is committed to the database (`@Observes(during = TransactionPhase.AFTER_SUCCESS)`), eliminating race conditions. + +```java +// JpaDatabaseTaskStore fires CDI event after persist +if (task.status().state().isFinal()) { + taskFinalizedEvent.fire(new TaskFinalizedEvent(task.id(), task)); +} + +// ReplicatedQueueManager observes it after transaction commit +public void onTaskFinalized( + @Observes(during = TransactionPhase.AFTER_SUCCESS) TaskFinalizedEvent event) { + QueueClosedEvent closedEvent = new QueueClosedEvent(event.getTaskId()); + replicationStrategy.send(event.getTaskId(), closedEvent); +} +``` + +No configuration is required — the poison pill mechanism works automatically when using `JpaDatabaseTaskStore` together with `ReplicatedQueueManager`. + +### Grace Period + +A configurable grace period prevents queue recreation for tasks that were recently finalized, accommodating Kafka consumer lag and network delays: + +```properties +# Grace period for handling late-arriving events (default: 15 seconds) +a2a.replication.grace-period-seconds=15 +``` + +When a replicated event arrives after a task is finalized, the system checks if the task is still within the grace period before creating a new queue. Increase the grace period in high-latency networks; decrease it for systems with low latency and high task turnover. + +## Alternative Message Brokers + +While Kafka is the primary tested broker, MicroProfile Reactive Messaging supports other brokers: + +### Apache Pulsar + +```properties +mp.messaging.outgoing.replicated-events-out.connector=smallrye-pulsar +mp.messaging.incoming.replicated-events-in.connector=smallrye-pulsar +pulsar.client.serviceUrl=pulsar://localhost:6650 +``` + +### AMQP (RabbitMQ, etc.) + +```properties +mp.messaging.outgoing.replicated-events-out.connector=smallrye-amqp +mp.messaging.incoming.replicated-events-in.connector=smallrye-amqp +amqp-host=localhost +amqp-port=5672 +``` + +> **Note:** Alternative message brokers have not been tested in this project yet. + +### WildFly/Jakarta EE Servers + +For non-Quarkus environments, configure MicroProfile Reactive Messaging according to your application server's documentation. + +## Custom Replication Strategies + +You can implement your own `ReplicationStrategy` if MicroProfile Reactive Messaging does not fit your environment: + +```java +@ApplicationScoped +public class CustomReplicationStrategy implements ReplicationStrategy { + + @Override + public void send(String taskId, Event event) { + // Your replication logic (e.g., database, REST API, etc.) + } +} +``` + +## Monitoring + +### Logging + +Enable debug logging to monitor replication activity: + +```properties +quarkus.log.category."org.a2aproject.sdk.extras.queuemanager.replicated".level=DEBUG +quarkus.log.category."org.a2aproject.sdk.extras.taskstore.database.jpa".level=DEBUG +``` + +Example log entries: + +``` +Task abc-123 is in final state, firing TaskFinalizedEvent +Task abc-123 finalized - sending poison pill (QueueClosedEvent) after transaction commit +Skipping replicated event for inactive task abc-123 +``` + +### Health Checks + +When using Quarkus, the module integrates with MicroProfile Health: + +```properties +quarkus.messaging.kafka.health.timeout=5s +``` + +### CDI Event Monitoring + +You can monitor replicated events by observing CDI events: + +```java +@ApplicationScoped +public class ReplicationMonitor { + + public void onReplicatedEvent(@Observes ReplicatedEventQueueItem replicatedEvent) { + LOGGER.info("Received replicated event for task: " + replicatedEvent.getTaskId()); + } +} +``` diff --git a/docs/content/1.1.0.Final/extras/storage.md b/docs/content/1.1.0.Final/extras/storage.md new file mode 100644 index 000000000..15140b67b --- /dev/null +++ b/docs/content/1.1.0.Final/extras/storage.md @@ -0,0 +1,163 @@ +--- +title: Storage & Persistence +description: JPA-backed TaskStore and PushNotificationConfigStore for the A2A Java SDK, providing database persistence for production and load-balanced deployments. +layout: page +--- + +# Storage & Persistence + +The default SDK uses in-memory stores that are lost on restart. For production deployments — especially load-balanced environments — replace them with JPA-backed stores that persist data to a relational database. + +Both JPA stores use the Jakarta Persistence API (JPA 3.0+), making them suitable for any JPA provider and Jakarta EE application server. They share the same persistence unit name (`a2a-java`) and database configuration, so they can share a single datasource. + +> **Note:** Stored objects are serialized to JSON according to the current A2A specification version. Future specification versions may change the format. These stores are intended for the operational lifetime of tasks, not long-term archival. If you wish to keep objects stored between protocol versions, you may have to implement migration of the stored data. + +## JPA Task Store + +Replaces `InMemoryTaskStore` with a database-backed implementation. The `JpaDatabaseTaskStore` is annotated to take precedence over the default — it is a drop-in replacement. + +### 1. Add Dependency + +```xml + + org.a2aproject.sdk + a2a-java-extras-task-store-database-jpa + +``` + +### 2. Configure Database + +#### For Quarkus Reference Servers + +Add to your `application.properties`: + +```properties +quarkus.datasource.db-kind=postgresql +quarkus.datasource.jdbc.url=jdbc:postgresql://localhost:5432/a2a_db +quarkus.datasource.username=your_username +quarkus.datasource.password=your_password +quarkus.hibernate-orm.database.generation=update +``` + +#### For WildFly/Jakarta EE Servers + +> **Note:** On a Jakarta EE application server, the JPA dependencies are provided by the runtime. Mark them with `provided` in your POM to avoid packaging conflicts. + +Create or update your `persistence.xml`: + +```xml + + + + java:jboss/datasources/A2ADataSource + + org.a2aproject.sdk.extras.taskstore.database.jpa.JpaTask + true + + + + + + + +``` + +### 3. Database Schema + +The module automatically creates the required table: + +```sql +CREATE TABLE a2a_tasks ( + task_id VARCHAR(255) PRIMARY KEY, + context_id VARCHAR(255), + state VARCHAR(255), + status_timestamp TIMESTAMP, + task_data TEXT NOT NULL, + finalized_at TIMESTAMP +); +``` + +### Persistence Unit Name + +The module uses the persistence unit name `"a2a-java"`. Ensure your `persistence.xml` defines a persistence unit with this name. + +## JPA Push Notification Config Store + +Replaces `InMemoryPushNotificationConfigStore` with a database-backed implementation, ensuring push notification subscriptions survive application restarts. The `JpaDatabasePushNotificationConfigStore` is annotated to take precedence over the default — it is a drop-in replacement. + +### 1. Add Dependency + +```xml + + org.a2aproject.sdk + a2a-java-extras-push-notification-config-store-database-jpa + +``` + +### 2. Configure Database + +Uses the same datasource and persistence unit (`a2a-java`) as the JPA Task Store. + +#### For Quarkus Reference Servers + +The same `application.properties` configuration as the JPA Task Store applies. + +#### For WildFly/Jakarta EE Servers + +Create or update your `persistence.xml`: + +```xml + + + + java:jboss/datasources/A2ADataSource + + org.a2aproject.sdk.extras.pushnotificationconfigstore.database.jpa.JpaPushNotificationConfig + true + + + + + + + +``` + +### 3. Database Schema + +The module automatically creates the required table with a composite primary key: + +```sql +CREATE TABLE a2a_push_notification_configs ( + task_id VARCHAR(255) NOT NULL, + config_id VARCHAR(255) NOT NULL, + task_data TEXT NOT NULL, + protocol_version VARCHAR(255), + created_at TIMESTAMP, + PRIMARY KEY (task_id, config_id) +); +``` + +## Using Both Stores Together + +When using both JPA stores, combine their entity classes in a single `persistence.xml`: + +```xml + + + + java:jboss/datasources/A2ADataSource + + org.a2aproject.sdk.extras.taskstore.database.jpa.JpaTask + org.a2aproject.sdk.extras.pushnotificationconfigstore.database.jpa.JpaPushNotificationConfig + true + + + + + + + +``` + +For Quarkus, no additional configuration is needed — both entities are discovered automatically. diff --git a/docs/content/1.1.0.Final/getting-started.md b/docs/content/1.1.0.Final/getting-started.md new file mode 100644 index 000000000..7cac5d145 --- /dev/null +++ b/docs/content/1.1.0.Final/getting-started.md @@ -0,0 +1,46 @@ +--- +title: Getting Started +description: Get started with the A2A Java SDK — overview, guides, and next steps. +layout: page +--- + +# Getting Started + +The A2A Java SDK is a multi-module Maven library for implementing the [Agent2Agent (A2A) Protocol](https://a2a-protocol.org/) in Java. It provides both client and server support for agent communication over JSON-RPC, gRPC, and REST transports. + +## What You Can Build + +- **A2A Servers** — Expose your Java agent as an A2A-compliant service that other agents and clients can discover and communicate with. +- **A2A Clients** — Connect to any A2A-compliant agent, with streaming, push notifications, and task management. + +## Quick Start + +Add the reference server dependency to your Maven project: + +```xml + + org.a2aproject.sdk + a2a-java-sdk-reference-jsonrpc + $\{org.a2aproject.sdk.version} + +``` + +Then implement an `AgentExecutor` and define an `AgentCard`. See the [Server Guide](server) for the full walkthrough. + +## Guides + +| Guide | Description | +|-------|-------------| +| [Server Guide](server) | Run your Java application as an A2A server | +| [Client Guide](client) | Communicate with A2A-compliant agents | +| [Configuration](configuration) | Config properties, MicroProfile Config, custom providers | +| [Task Authorization](authorization) | Per-user access control for multi-user deployments | +| [Backward Compatibility](compatibility) | Serve v1.0 and v0.3 clients simultaneously | +| [Extras](extras) | Optional add-ons: database storage, OpenTelemetry, HTTP clients | +| [BOMs](boms) | Dependency management with Bill of Materials | +| [Examples](examples) | Hello World walkthroughs and sample applications | + +## Requirements + +- Java 17+ +- Maven 3.8+ diff --git a/docs/content/server.md b/docs/content/1.1.0.Final/server.md similarity index 91% rename from docs/content/server.md rename to docs/content/1.1.0.Final/server.md index 378cbadfc..39fe727dd 100644 --- a/docs/content/server.md +++ b/docs/content/1.1.0.Final/server.md @@ -127,8 +127,8 @@ public class WeatherAgentExecutorProducer { agentEmitter.cancel(); return; } - if (task.getStatus().state() == TaskState.CANCELED || - task.getStatus().state() == TaskState.COMPLETED) { + if (task.status().state() == TaskState.CANCELED || + task.status().state() == TaskState.COMPLETED) { throw new TaskNotCancelableError(); } agentEmitter.cancel(); @@ -152,15 +152,15 @@ public class WeatherAgentExecutorProducer { ## 4. Configuration -See [Configuration]({site.url('configuration')}) for all config properties and tuning. +See [Configuration](configuration) for all config properties and tuning. ## 5. Task Authorization (Optional) -See [Task Authorization]({site.url('authorization')}) for per-user access control. +See [Task Authorization](authorization) for per-user access control. ## Backward Compatibility with v0.3 -See [Backward Compatibility]({site.url('compatibility')}) for multi-version modules, version routing, and v0.3 client support. +See [Backward Compatibility](compatibility) for multi-version modules, version routing, and v0.3 client support. ## Server Integrations diff --git a/docs/content/dev/authorization.md b/docs/content/dev/authorization.md new file mode 100644 index 000000000..e726cb5ee --- /dev/null +++ b/docs/content/dev/authorization.md @@ -0,0 +1,67 @@ +--- +title: Task Authorization +description: Per-user access control for A2A tasks — ownership, read/write checks, and user identity across transports. +layout: page +--- + +# Task Authorization + +> **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). + +## Implementing TaskAuthorizationProvider + +Implement `TaskAuthorizationProvider` to control per-user access: + +```java +@ApplicationScoped +public class MyTaskAuthorizationProvider implements TaskAuthorizationProvider { + + @Override + public boolean checkRead(ServerCallContext context, String taskId, TaskOperation op) { + return isOwner(context.getUser(), taskId); + } + + @Override + public boolean checkWrite(ServerCallContext context, String taskId, TaskOperation op) { + return isOwner(context.getUser(), taskId); + } + + @Override + public boolean checkCreate(ServerCallContext context, TaskOperation op) { + return context.getUser() != null && context.getUser().isAuthenticated(); + } + + @Override + public boolean isTaskRecorded(String taskId) { + return ownershipStore.containsKey(taskId); + } + + @Override + public void recordOwnership(ServerCallContext context, String taskId, TaskOperation op) { + if (context.getUser() != null) { + ownershipStore.put(taskId, context.getUser().getUsername()); + } + } +} +``` + +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` | +| `cancelTask`, `createTaskPushNotificationConfig`, `deleteTaskPushNotificationConfig` | `checkWrite` | +| `messageSend` / `messageSendStream` (existing task) | `checkWrite` | +| `messageSend` / `messageSendStream` (new task) | `checkCreate`, then `recordOwnership` | +| `listTasks` | `checkRead` per task | diff --git a/docs/content/dev/boms.md b/docs/content/dev/boms.md new file mode 100644 index 000000000..e0dc6e1b4 --- /dev/null +++ b/docs/content/dev/boms.md @@ -0,0 +1,120 @@ +--- +title: Bill of Materials (BOM) +description: Dependency management BOMs for the A2A Java SDK — SDK, Extras, and Reference BOMs. +layout: page +--- + +# Bill of Materials (BOM) + +The A2A Java SDK provides three BOMs for different use cases, so you can manage dependency versions in one place. + +## BOM Modules + +### SDK BOM + +**Artifact:** `org.a2aproject.sdk:a2a-java-sdk-bom` + +Includes all A2A SDK core modules (spec, server, client, transport), core third-party dependencies, Jakarta APIs, and test utilities. + +**Use this BOM when:** Building A2A agents with any framework (Quarkus, Spring Boot, vanilla Java, etc.) + +### Extras BOM + +**Artifact:** `org.a2aproject.sdk:a2a-java-sdk-extras-bom` + +Includes everything from the SDK BOM plus server-side enhancement modules (database persistence, distributed queue management, etc.). + +**Use this BOM when:** Building production A2A servers needing advanced server-side features beyond the core SDK. + +### Reference BOM + +**Artifact:** `org.a2aproject.sdk:a2a-java-sdk-reference-bom` + +Includes everything from the SDK BOM plus the Quarkus BOM (complete Quarkus platform), A2A reference implementation modules, and the TCK module for testing. + +**Use this BOM when:** Building Quarkus-based A2A agents or reference implementations. + +## Usage + +### SDK BOM (Any Framework) + +```xml + + + + org.a2aproject.sdk + a2a-java-sdk-bom + $\{org.a2aproject.sdk.version} + pom + import + + + + + + + + org.a2aproject.sdk + a2a-java-sdk-server-common + + + org.a2aproject.sdk + a2a-java-sdk-transport-jsonrpc + + +``` + +### Extras BOM (Database Persistence, Distributed Deployments) + +```xml + + + + org.a2aproject.sdk + a2a-java-sdk-extras-bom + $\{org.a2aproject.sdk.version} + pom + import + + + + + + + org.a2aproject.sdk + a2a-java-sdk-server-common + + + org.a2aproject.sdk + a2a-java-extras-task-store-database-jpa + + +``` + +### Reference BOM (Quarkus) + +```xml + + + + org.a2aproject.sdk + a2a-java-sdk-reference-bom + $\{org.a2aproject.sdk.version} + pom + import + + + + + + + + org.a2aproject.sdk + a2a-java-sdk-reference-jsonrpc + + + io.quarkus + quarkus-arc + + +``` diff --git a/docs/content/dev/client.md b/docs/content/dev/client.md new file mode 100644 index 000000000..c39bb24f9 --- /dev/null +++ b/docs/content/dev/client.md @@ -0,0 +1,197 @@ +--- +title: A2A Client Guide +description: Communicate with A2A-compliant agents using the A2A Java SDK client. +layout: page +--- + +# A2A Client + +The A2A Java SDK provides a Java client for communicating with any A2A-compliant agent. Supports JSON-RPC 2.0, gRPC, and HTTP+JSON/REST transports. + +## 1. Add the Client Dependency + +```xml + + org.a2aproject.sdk + a2a-java-sdk-client + + $\{org.a2aproject.sdk.version} + +``` + +The client artifact includes the JSON-RPC transport by default. For gRPC or REST, add the corresponding transport: + +```xml + + + org.a2aproject.sdk + a2a-java-sdk-client-transport-grpc + $\{org.a2aproject.sdk.version} + + + + + org.a2aproject.sdk + a2a-java-sdk-client-transport-rest + $\{org.a2aproject.sdk.version} + +``` + +## 2. Create a Client + +```java +// Resolve the agent card from the server +AgentCard agentCard = A2ACardResolver.builder() + .baseUrl("http://localhost:1234") + .build() + .getAgentCard(); + +// Configure accepted output modes +ClientConfig clientConfig = new ClientConfig.Builder() + .setAcceptedOutputModes(List.of("text")) + .build(); + +// Define event consumers +List> consumers = List.of( + (event, card) -> { + if (event instanceof MessageEvent messageEvent) { + // handle message + } else if (event instanceof TaskEvent taskEvent) { + // handle task + } else if (event instanceof TaskUpdateEvent updateEvent) { + // handle task update + } + } +); + +// Build the client +Client client = Client + .builder(agentCard) + .clientConfig(clientConfig) + .withTransport(JSONRPCTransport.class, new JSONRPCTransportConfig()) + .addConsumers(consumers) + .streamingErrorHandler(error -> { /* handle errors */ }) + .build(); +``` + +## 3. Send Messages + +```java +// Send a text message (streaming used automatically if supported) +Message message = A2A.toUserMessage("tell me a joke"); +client.sendMessage(message); + +// Send with per-call custom consumers +client.sendMessage(message, customConsumers, customErrorHandler); + +// Send with a call context +client.sendMessage(message, clientCallContext); +``` + +## Task Management + +```java +// Get task state +Task task = client.getTask(new TaskQueryParams("task-1234")); +Task task = client.getTask(new TaskQueryParams("task-1234", 10)); // with history limit + +// Cancel a task +Task cancelled = client.cancelTask(new TaskIdParams("task-1234")); + +// Subscribe to an ongoing task +client.subscribeToTask(new TaskIdParams("task-1234")); +client.subscribeToTask(taskIdParams, customConsumers, customErrorHandler); + +// Retrieve the server agent card +AgentCard serverCard = client.getAgentCard(); +``` + +## Push Notifications + +```java +// Set a push notification configuration +PushNotificationConfig pushConfig = PushNotificationConfig.builder() + .url("https://example.com/callback") + .authenticationInfo(new AuthenticationInfo(List.of("jwt"), null)) + .build(); + +TaskPushNotificationConfig taskConfig = TaskPushNotificationConfig.builder() + .taskId("task-1234") + .pushNotificationConfig(pushConfig) + .build(); + +client.createTaskPushNotificationConfiguration(taskConfig); + +// Get a specific configuration +TaskPushNotificationConfig config = client.getTaskPushNotificationConfiguration( + new GetTaskPushNotificationConfigParams("task-1234", "config-4567")); + +// List configurations +List configs = + client.listTaskPushNotificationConfigurations( + new ListTaskPushNotificationConfigParams("task-1234")); + +// Delete a configuration +client.deleteTaskPushNotificationConfigurations( + new DeleteTaskPushNotificationConfigParams("task-1234", "config-4567")); +``` + +## Transport Configuration + +### JSON-RPC with a Custom HTTP Client + +```java +// Use a custom JDK HTTP client +HttpClient jdkHttpClient = HttpClient.newBuilder() + .connectTimeout(Duration.ofSeconds(5)) + .followRedirects(HttpClient.Redirect.NORMAL) + .build(); + +Client client = Client + .builder(agentCard) + .withTransport(JSONRPCTransport.class, + new JSONRPCTransportConfig(new JdkA2AHttpClient(jdkHttpClient))) + .build(); +``` + +### REST with a Custom HTTP Client + +```java +A2AHttpClient customHttpClient = ... + +Client client = Client + .builder(agentCard) + .withTransport(RestTransport.class, new RestTransportConfig(customHttpClient)) + .build(); +``` + +### gRPC + +```java +Function channelFactory = agentUrl -> + ManagedChannelBuilder.forTarget(agentUrl).build(); + +Client client = Client + .builder(agentCard) + .withTransport(GrpcTransport.class, new GrpcTransportConfig(channelFactory)) + .build(); +``` + +### Multiple Transports + +```java +Client client = Client + .builder(agentCard) + .withTransport(GrpcTransport.class, new GrpcTransportConfig(channelFactory)) + .withTransport(JSONRPCTransport.class, new JSONRPCTransportConfig()) + .withTransport(RestTransport.class, new RestTransportConfig()) + .build(); +``` + +## Communicating with v0.3 Agents + +See [Backward Compatibility](compatibility#client-communicating-with-v03-agents) for using `Client_v0_3` with older protocol agents. + +## Examples + +See [Examples](examples) for Hello World walkthroughs and sample applications. diff --git a/docs/content/dev/compatibility.md b/docs/content/dev/compatibility.md new file mode 100644 index 000000000..aae404e3e --- /dev/null +++ b/docs/content/dev/compatibility.md @@ -0,0 +1,124 @@ +--- +title: Backward Compatibility +description: Serve v1.0 and v0.3 A2A protocol versions simultaneously — multi-version modules, version routing, and v0.3 client support. +layout: page +--- + +# Backward Compatibility with v0.3 + +Add compat modules alongside v1.0 modules to serve both protocol versions simultaneously. No changes to your `AgentExecutor` are needed. + +## Server: Multi-Version Module (recommended) + +```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} + +``` + +## Server: 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} + +``` + +## 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. + +## Client: Communicating with v0.3 Agents + +Use `Client_v0_3` to communicate with agents that only support protocol v0.3: + +```xml + + org.a2aproject.sdk + a2a-java-sdk-compat-0.3-client + $\{org.a2aproject.sdk.version} + + + org.a2aproject.sdk + a2a-java-sdk-compat-0.3-client-transport-jsonrpc + $\{org.a2aproject.sdk.version} + +``` + +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(); + +AgentInterface v03Interface = card.supportedInterfaces().stream() + .filter(i -> A2AProtocol_v0_3.PROTOCOL_VERSION.equals(i.protocolVersion())) + .findFirst().orElseThrow(); + +Client_v0_3 client = ClientBuilder_v0_3.forUrl(v03Interface.url()) + .withTransport(JSONRPCTransport_v0_3.class, new JSONRPCTransportConfigBuilder_v0_3()) + .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. diff --git a/docs/content/dev/configuration.md b/docs/content/dev/configuration.md new file mode 100644 index 000000000..b164bf3cf --- /dev/null +++ b/docs/content/dev/configuration.md @@ -0,0 +1,150 @@ +--- +title: Configuration +description: Configure the A2A Java SDK — properties, MicroProfile Config integration, custom providers. +layout: page +--- + +# Configuration + +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` integration to override defaults via `application.properties`, environment variables, or system properties +- **Spring/other frameworks**: Implement a custom `A2AConfigProvider` (see [Custom Config Providers](#custom-config-providers) below) +- **Reference implementations**: Already include the MicroProfile Config integration + +## Configuration Properties + +### Executor Settings + +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 + +```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 +``` + +### Tuning Guidelines + +- **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. + +## MicroProfile Config Integration + +Add the integration dependency to override configuration via standard MicroProfile Config sources: + +```xml + + org.a2aproject.sdk + a2a-java-sdk-microprofile-config + $\{org.a2aproject.sdk.version} + +``` + +Once added, you can set any A2A property through: + +**application.properties:** +```properties +a2a.executor.core-pool-size=10 +a2a.executor.max-pool-size=100 +a2a.blocking.agent.timeout.seconds=60 +``` + +**Environment variables:** +```bash +export A2A_EXECUTOR_CORE_POOL_SIZE=10 +export A2A_BLOCKING_AGENT_TIMEOUT_SECONDS=60 +``` + +**System properties:** +```bash +java -Da2a.executor.core-pool-size=10 -jar your-app.jar +``` + +### Configuration Fallback Chain + +``` +MicroProfile Config Sources (application.properties, env vars, -D flags) + | (not found?) +DefaultValuesConfigProvider + -> Scans classpath for ALL META-INF/a2a-defaults.properties files + -> Merges all discovered properties together + -> Throws exception if duplicate keys found + | (property exists?) +Return merged default value + | (not found?) +IllegalArgumentException +``` + +All `META-INF/a2a-defaults.properties` files (from server-common, extras modules, etc.) are loaded and merged together by `DefaultValuesConfigProvider` at startup. This is not a sequential fallback chain, but a single merged set of defaults. + +### Framework Compatibility + +The MicroProfile Config integration works with any MicroProfile Config implementation: + +- **Quarkus** -- Built-in MicroProfile Config support +- **Helidon** -- Built-in MicroProfile Config support +- **Open Liberty** -- Built-in MicroProfile Config support +- **WildFly/JBoss EAP** -- Add `smallrye-config` dependency +- **Other Jakarta EE servers** -- Add MicroProfile Config implementation + +## Custom Config Providers + +If you're using a different framework (Spring, Micronaut, etc.), implement your own `A2AConfigProvider`: + +```java +@ApplicationScoped +@Alternative +@Priority(100) // Higher than MicroProfileConfigProvider's priority of 50 +public class MyConfigProvider implements A2AConfigProvider { + + private final Properties customConfig; + + @Inject + DefaultValuesConfigProvider defaultValues; + + MyConfigProvider() { + customConfig = loadFromYourFramework(); + } + + @Override + public String getValue(String name) { + String value = customConfig.getProperty(name); + if (value != null) { + return value; + } + return defaultValues.getValue(name); + } + + @Override + public Optional getOptionalValue(String name) { + String value = customConfig.getProperty(name); + if (value != null) { + return Optional.of(value); + } + return defaultValues.getOptionalValue(name); + } +} +``` + +**Note:** The reference server implementations (Quarkus-based) automatically include the MicroProfile Config integration, so properties work out of the box in `application.properties`. diff --git a/docs/content/dev/examples.md b/docs/content/dev/examples.md new file mode 100644 index 000000000..9d5b0fb40 --- /dev/null +++ b/docs/content/dev/examples.md @@ -0,0 +1,149 @@ +--- +title: Examples +description: Hello World examples for the A2A Java SDK — server and client walkthroughs with transport selection and OpenTelemetry. +layout: page +--- + +# Examples + +## Prerequisites + +- Java 17 or higher +- Python 3.8 or higher +- [uv](https://github.com/astral-sh/uv) (recommended Python package installer) +- Git + +## Hello World Server + +This example runs a Java A2A server that a Python client can talk to. + +### Start the Java Server + +```bash +cd examples/helloworld/server +mvn quarkus:dev +``` + +### Transport Protocol Selection + +Select the transport protocol via the `quarkus.agentcard.protocol` property: + +```bash +# JSON-RPC (default) +mvn quarkus:dev + +# gRPC +mvn quarkus:dev -Dquarkus.agentcard.protocol=GRPC + +# HTTP+JSON/REST +mvn quarkus:dev -Dquarkus.agentcard.protocol=HTTP+JSON +``` + +You can also set the default in `src/main/resources/application.properties`: +```properties +quarkus.agentcard.protocol=HTTP+JSON +``` + +### Run the Python Client + +The Python client is part of the [a2a-samples](https://github.com/a2aproject/a2a-samples) project: + +```bash +git clone https://github.com/a2aproject/a2a-samples.git +cd a2a-samples/samples/python/agents/helloworld + +uv venv +source .venv/bin/activate +uv pip install -e . +uv run test_client.py +``` + +The client connects to `http://localhost:9999`, fetches the agent card, sends a regular message, then sends the same message as a streaming request. + +## Hello World Client + +This example runs a Java A2A client that talks to a Python server. + +### Start the Python Server + +```bash +git clone https://github.com/a2aproject/a2a-samples.git +cd a2a-samples/samples/python/agents/helloworld + +uv venv +source .venv/bin/activate +uv pip install -e . +uv run . +``` + +The server starts on `http://localhost:9999`. You can also use the [Java server example](#hello-world-server) instead. + +### Run the Java Client + +First build the SDK, then run the client: + +```bash +# From the a2a-java root +mvn clean install + +# Run the client +cd examples/helloworld/client +mvn exec:java +``` + +#### Transport Protocol Selection + +```bash +# JSON-RPC (default) +mvn exec:java + +# gRPC +mvn exec:java -Dquarkus.agentcard.protocol=GRPC + +# HTTP+JSON/REST +mvn exec:java -Dquarkus.agentcard.protocol=HTTP+JSON +``` + +The protocol you select on the client must match the protocol configured on the server. + +#### Using JBang + +Alternatively, run the client with [JBang](https://www.jbang.dev/) (no Maven required): + +```bash +jbang examples/helloworld/client/src/main/java/org/a2aproject/sdk/examples/helloworld/HelloWorldRunner.java +``` + +Pass transport and OpenTelemetry flags the same way: +```bash +jbang examples/helloworld/client/src/main/java/org/a2aproject/sdk/examples/helloworld/HelloWorldRunner.java \ + -Dquarkus.agentcard.protocol=GRPC -Dopentelemetry=true +``` + +## OpenTelemetry (Optional) + +Both the server and client support distributed tracing with OpenTelemetry. + +### Server with OpenTelemetry + +```bash +cd examples/helloworld/server +mvn quarkus:dev -Popentelemetry +``` + +Quarkus Dev Services automatically starts a Grafana observability stack. Open Grafana at `http://localhost:3001` (credentials: admin/admin) and view traces in the "Explore" section using the Tempo data source. + +### Client with OpenTelemetry + +```bash +cd examples/helloworld/client +mvn exec:java -Dopentelemetry=true +``` + +The client expects an OpenTelemetry collector on port 5317. The easiest way is to run the Java server with `-Popentelemetry` (which starts the collector automatically), then run the client with `-Dopentelemetry=true` for end-to-end traces. + +For more information, see the [OpenTelemetry extras module](extras/opentelemetry). + +## More Examples + +- [a2a-samples repository](https://github.com/a2aproject/a2a-samples/tree/main/samples/java/agents) — Additional agent examples in Java and other languages diff --git a/docs/content/dev/extras.md b/docs/content/dev/extras.md new file mode 100644 index 000000000..2a4478b5e --- /dev/null +++ b/docs/content/dev/extras.md @@ -0,0 +1,45 @@ +--- +title: Extras +description: Optional add-ons for the A2A Java SDK including alternative HTTP clients, database-backed stores, distributed queue replication, and OpenTelemetry observability. +layout: page +--- + +# Extras + +The A2A Java SDK ships with sensible defaults — an in-memory task store, an in-memory push notification config store, a single-instance queue manager, and a JDK HttpClient. The **extras** modules are drop-in replacements that swap these defaults for production-grade alternatives. + +All extras use **Java SPI** or **CDI alternative** discovery, so adding the dependency is typically all you need — no code changes required. + +## BOM + +Import the extras BOM to manage versions: + +```xml + + + + org.a2aproject.sdk + a2a-java-extras-bom + $\{org.a2aproject.sdk.version} + pom + import + + + +``` + +## [HTTP Clients](extras/http-clients) + +Alternative `A2AHttpClient` implementations. The **Vert.x HTTP Client** (`a2a-java-sdk-http-client-vertx`) replaces the default JDK HttpClient with a reactive, non-blocking Vert.x WebClient — recommended for Quarkus, reactive frameworks, and high-throughput scenarios. An **Android HTTP Client** (`a2a-java-sdk-http-client-android`) is also available for mobile applications. + +## [Storage & Persistence](extras/storage) + +JPA-backed replacements for the in-memory stores, providing database persistence for production and load-balanced deployments. Includes a **JPA Task Store** (`a2a-java-extras-task-store-database-jpa`) and a **JPA Push Notification Config Store** (`a2a-java-extras-push-notification-config-store-database-jpa`). Both use Jakarta Persistence API (JPA 3.0+) and share the same datasource configuration. + +## [Replicated Queue Manager](extras/replicated-queue-manager) + +Replaces the default `InMemoryQueueManager` with event replication across multiple A2A server instances via message brokers. Required for multi-instance deployments. The core module (`a2a-java-queue-manager-replicated-core`) pairs with a MicroProfile Reactive Messaging strategy (`a2a-java-queue-manager-replication-mp-reactive`) supporting Apache Kafka, Pulsar, or AMQP. You can also write your own `ReplicationStrategy`. + +## [OpenTelemetry](extras/opentelemetry) + +Distributed tracing, metrics, and context propagation for A2A servers and clients using OpenTelemetry. The server module (`a2a-java-sdk-opentelemetry-server`) adds automatic span creation for all protocol methods with context propagation across async boundaries. Client modules (`a2a-java-sdk-opentelemetry-client`, `a2a-java-sdk-opentelemetry-client-propagation`) instrument A2A client operations. diff --git a/docs/content/dev/extras/http-clients.md b/docs/content/dev/extras/http-clients.md new file mode 100644 index 000000000..c47050c08 --- /dev/null +++ b/docs/content/dev/extras/http-clients.md @@ -0,0 +1,207 @@ +--- +title: HTTP Clients +description: Alternative HTTP client implementations for the A2A Java SDK, including Vert.x WebClient for reactive applications and an Android-specific client. +layout: page +--- + +# HTTP Clients + +The A2A SDK uses an `A2AHttpClient` abstraction for all HTTP communication — fetching agent cards, making transport calls, and Server-Sent Events (SSE) streaming. By default, the SDK uses a JDK 11+ HttpClient implementation. These extras provide alternative implementations. + +## Vert.x HTTP Client + +Replaces the default `JdkA2AHttpClient` with a [Vert.x WebClient](https://vertx.io/docs/vertx-web-client/java/)-based implementation (`VertxA2AHttpClient`) for reactive, non-blocking HTTP communication. + +### When to Use + +**Recommended for:** + +- Quarkus applications (Vert.x is already included) +- Reactive applications using Vert.x or reactive frameworks +- High-throughput scenarios with many concurrent requests +- Applications requiring efficient SSE streaming +- Cloud-native deployments optimizing for resource usage + +**Not needed for** simple, low-volume applications or environments where JDK HttpClient performs adequately. + +### What It Solves + +| | JDK HttpClient | Vert.x WebClient | +|--|----------------|-------------------| +| **I/O model** | Platform threads, blocking | Event loop, non-blocking | +| **Memory** | Higher (one thread per connection) | Lower (shared event loop) | +| **HTTP/2** | Manual configuration | Automatic ALPN negotiation | +| **Reactive integration** | Requires bridging | Native | + +### 1. Add Dependency + +```xml + + org.a2aproject.sdk + a2a-java-sdk-http-client-vertx + +``` + +For non-Quarkus environments, also add `vertx-web-client`: + +```xml + + io.vertx + vertx-web-client + +``` + +### 2. Automatic Discovery (No Code Changes) + +The `VertxA2AHttpClientProvider` has **priority 100** (vs. 0 for the JDK client). The SDK's `A2AHttpClientFactory` uses `ServiceLoader` to discover and select the highest-priority provider available: + +```java +// No changes needed — A2A SDK automatically uses VertxA2AHttpClient +A2ACardResolver resolver = A2ACardResolver.builder().baseUrl("http://localhost:9999").build(); +AgentCard card = resolver.getAgentCard(); // Uses Vert.x under the hood + +Client client = Client.builder(card) + .withTransport(JSONRPCTransport.class, new JSONRPCTransportConfig()) + .build(); +``` + +If Vert.x classes are not on the classpath, the provider returns priority `-1` and the SDK falls back to the JDK HttpClient. + +### Usage Examples + +#### GET Request + +```java +try (A2AHttpClient client = A2AHttpClientFactory.create()) { + A2AHttpResponse response = client.createGet() + .url("https://api.example.com/data") + .addHeader("Authorization", "Bearer token") + .get(); + + if (response.success()) { + System.out.println(response.body()); + } +} +``` + +#### POST Request with JSON Body + +```java +try (A2AHttpClient client = A2AHttpClientFactory.create()) { + A2AHttpResponse response = client.createPost() + .url("https://api.example.com/submit") + .addHeader("Content-Type", "application/json") + .body("{\"key\":\"value\"}") + .post(); + + System.out.println("Status: " + response.status()); +} +``` + +#### Server-Sent Events (SSE) Streaming + +```java +try (A2AHttpClient client = A2AHttpClientFactory.create()) { + CompletableFuture future = client.createGet() + .url("https://api.example.com/stream") + .getAsyncSSE( + message -> System.out.println("Received: " + message), + error -> error.printStackTrace(), + () -> System.out.println("Stream complete") + ); + + // Do other work while streaming... + future.join(); +} +``` + +### CDI / Quarkus Integration + +In Quarkus or other CDI environments, the `VertxA2AHttpClient` automatically discovers and reuses the CDI-managed `Vertx` instance so that your entire application shares the same event loop. + +For non-CDI environments, you can share a Vert.x instance manually: + +```java +Vertx vertx = Vertx.vertx(); + +try { + try (VertxA2AHttpClient client = new VertxA2AHttpClient(vertx)) { + A2AHttpResponse response = client.createGet() + .url("https://example.com") + .get(); + } +} finally { + vertx.close(); +} +``` + +### Lifecycle Management + +| Usage | Behavior on close | +|-------|--------------------| +| Standalone (`new VertxA2AHttpClient()`) | Both `WebClient` and `Vertx` are closed | +| CDI/Quarkus (`new VertxA2AHttpClient(injectedVertx)`) | Only `WebClient` is closed; `Vertx` remains open | + +Always use try-with-resources to avoid leaking `Vertx` instances. + +### Thread Safety + +- **Client instances** are thread-safe — multiple threads can use the same client +- **Builder instances** are NOT thread-safe — create separate builders per thread +- All I/O operations execute on Vert.x event loop threads + +### Performance Notes + +Synchronous methods (`.get()`, `.post()`, `.delete()`) block the calling thread despite using Vert.x's async API internally. Vert.x still provides benefits at the network layer: non-blocking I/O, efficient connection pooling, and lower thread usage overall. + +Async methods (`.getAsyncSSE()`, `.postAsyncSSE()`) are truly async — they return a `CompletableFuture` immediately. + +### Troubleshooting + +**Client not being used** — If logs show `JdkA2AHttpClient` instead of `VertxA2AHttpClient`, verify Vert.x WebClient is on the classpath: + +```bash +mvn dependency:tree | grep vertx-web-client +``` + +**ClassNotFoundException for Vert.x** — Add the `vertx-web-client` dependency (see above). For Quarkus, ensure `quarkus-vertx` is present. + +**Memory leaks** — Always use try-with-resources when creating `VertxA2AHttpClient` with the no-args constructor. + +### Version Compatibility + +- **Java**: 17+ +- **Vert.x**: 4.x (tested with 4.5.0+) +- **Quarkus**: Any version using Vert.x 4.x +- **Jakarta EE**: 9.0+ + +## Android HTTP Client + + +HTTP client implementation for Android applications, using `HttpURLConnection` under the hood — no external dependencies required. + +### When to Use + +- Android applications targeting API level 24+ (Android 7.0+) +- Mobile apps that need to communicate with A2A agents + +### 1. Add Dependency + +```xml + + org.a2aproject.sdk + a2a-java-sdk-http-client-android + +``` + + +### 2. Automatic Discovery (No Code Changes) + +The `AndroidA2AHttpClientProvider` has **priority 110** and detects the Android runtime automatically. On non-Android runtimes, the provider returns priority `-1` and the SDK falls back to the next available client. + +### Features + +- **GET, POST, DELETE** requests with fluent builder API +- **SSE streaming** via `getAsyncSSE()` and `postAsyncSSE()` — async operations run on a dedicated cached thread pool (`A2A-Android-Net`) +- **Timeouts**: 15 s connection, 60 s read +- **Response size limit**: 10 MB diff --git a/docs/content/dev/extras/opentelemetry.md b/docs/content/dev/extras/opentelemetry.md new file mode 100644 index 000000000..7b6dcb68c --- /dev/null +++ b/docs/content/dev/extras/opentelemetry.md @@ -0,0 +1,189 @@ +--- +title: OpenTelemetry +description: OpenTelemetry observability integration for the A2A Java SDK with distributed tracing, context propagation, and metrics for servers and clients. +layout: page +--- + +# OpenTelemetry + +Adds distributed tracing, metrics, and context propagation to A2A servers and clients using [OpenTelemetry](https://opentelemetry.io/). + +## Features + +- **Distributed Tracing**: Automatic span creation for all A2A protocol methods +- **Context Propagation**: OpenTelemetry trace context propagation across async operations +- **Request/Response Logging**: Optional extraction of request and response data into spans +- **Error Tracking**: Automatic error status and error type attributes on failures + +## Modules + +| Module | Artifact ID | Description | +|--------|-------------|-------------| +| Common | `a2a-java-sdk-opentelemetry-common` | Shared utilities and constants | +| Server | `a2a-java-sdk-opentelemetry-server` | Server-side tracing | +| Client | `a2a-java-sdk-opentelemetry-client` | Client-side instrumentation | +| Client Propagation | `a2a-java-sdk-opentelemetry-client-propagation` | Context propagation for async client operations | + +## Server + +The `OpenTelemetryRequestHandlerDecorator` wraps the default request handler to create spans for every A2A protocol method, with automatic error tracking. + +### 1. Add Dependency + +```xml + + org.a2aproject.sdk + a2a-java-sdk-opentelemetry-server + +``` + +### Architecture + +``` +Client Request + | +OpenTelemetryRequestHandlerDecorator + | (creates span) +Default RequestHandler + | +Agent Execution (with context propagation) + | +Response +``` + +``` +HTTP Request (with trace headers) + | +OpenTelemetry extracts context + | +Span created for A2A method + | +ManagedExecutor propagates context + | +Async agent execution (maintains trace context) + | +Response (with trace headers) +``` + +### Context-Aware Async Executor + +> **Note:** The `AsyncManagedExecutorProducer` is provided by the **Quarkus reference server** ([`reference/common`](https://github.com/a2aproject/a2a-java/blob/main/reference/common/src/main/java/org/a2aproject/sdk/server/common/quarkus/AsyncManagedExecutorProducer.java)), not the OpenTelemetry module. It is documented here because it enables context propagation (including trace context) across async boundaries. + + +The reference server replaces the default `AsyncExecutorProducer` with `AsyncManagedExecutorProducer`: + +- **Priority 20**: takes precedence over the default executor (priority 10) +- **Automatic activation**: no configuration needed — included automatically in the Quarkus reference server +- **Context propagation**: uses MicroProfile Context Propagation to maintain trace context and CDI request context across async boundaries + +> **Note:** Unlike the default `AsyncExecutorProducer`, the `AsyncManagedExecutorProducer` does not use the `a2a.executor.*` configuration properties. Pool sizing is controlled by the container's `ManagedExecutor` configuration. + +#### Quarkus Configuration + +```properties +quarkus.thread-pool.core-threads=10 +quarkus.thread-pool.max-threads=50 +quarkus.thread-pool.queue-size=100 +``` + +For other runtimes, consult your MicroProfile Context Propagation implementation documentation. + +### Span Attributes + +The following attributes are automatically added to spans: + +| Attribute | Description | +|-----------|-------------| +| `gen_ai.agent.a2a.request` | Request parameters (if extraction enabled) | +| `gen_ai.agent.a2a.response` | Response data (if extraction enabled) | +| `error.type` | Error message (on failures) | + +### Request/Response Extraction + +Enable request and response data extraction in spans using JVM system properties: + +``` +# Extract request parameters into span attributes (disabled by default) +-Dorg.a2aproject.sdk.server.extract.request=true + +# Extract response data into span attributes (disabled by default) +-Dorg.a2aproject.sdk.server.extract.response=true +``` + +> **Warning:** Extracting request/response data may expose sensitive information in traces. Use with caution in production environments. + +## Client + +### Instrumentation + +Adds OpenTelemetry spans to A2A client operations: + +```xml + + org.a2aproject.sdk + a2a-java-sdk-opentelemetry-client + +``` + +### Context Propagation + +For trace context propagation across async client boundaries: + +```xml + + org.a2aproject.sdk + a2a-java-sdk-opentelemetry-client-propagation + +``` + +## Troubleshooting + +### Context Not Propagating + +**Symptom:** Spans in async tasks are not linked to parent spans. + +**Solution:** Ensure the OpenTelemetry server module is included and check logs for: + +``` +Initializing ManagedExecutor for async operations with CDI context propagation +``` + +### ManagedExecutor Not Available + +**Symptom:** `IllegalStateException: ManagedExecutor not injected - ensure MicroProfile Context Propagation is available` + +**Solution:** Ensure your runtime provides MicroProfile Context Propagation. For Quarkus, add: + +```xml + + io.quarkus + quarkus-smallrye-context-propagation + +``` + +### Performance Impact + +- Disable request/response extraction in production unless needed +- Configure sampling rate to reduce trace volume +- Ensure your OpenTelemetry collector is properly sized + +## Best Practices + +1. Configure appropriate **sampling rates** for production environments +2. Disable request/response extraction if handling **sensitive data** +3. Add **service name and version** as resource attributes +4. Use **batch processors** in your OpenTelemetry collector to reduce network overhead +5. Monitor the OpenTelemetry **collector's health** and performance + +## Dependencies + +- MicroProfile Telemetry 2.0.1+ +- MicroProfile Context Propagation 1.3+ +- OpenTelemetry API +- A2A Server Common + +## See Also + +- [OpenTelemetry Documentation](https://opentelemetry.io/docs/) +- [MicroProfile Telemetry Specification](https://github.com/eclipse/microprofile-telemetry) +- [MicroProfile Context Propagation](https://github.com/eclipse/microprofile-context-propagation) diff --git a/docs/content/dev/extras/replicated-queue-manager.md b/docs/content/dev/extras/replicated-queue-manager.md new file mode 100644 index 000000000..2d650bd93 --- /dev/null +++ b/docs/content/dev/extras/replicated-queue-manager.md @@ -0,0 +1,288 @@ +--- +title: Replicated Queue Manager +description: Distributed QueueManager implementation for the A2A Java SDK that replicates events across multiple server instances using message brokers like Apache Kafka. +layout: page +--- + +# Replicated Queue Manager + +Replaces the default `InMemoryQueueManager` with a queue manager that replicates events across multiple A2A server instances. Required for multi-instance (load-balanced) deployments where all nodes must see every event. + +## Architecture + +The replicated queue manager has two core components: + +- **`ReplicatedQueueManager`** — wraps the default `InMemoryQueueManager` and handles event replication. It is annotated to take precedence over the default, making it a drop-in replacement. +- **`ReplicationStrategy`** — interface for pluggable replication implementations. A `ReplicationStrategy` **must** be provided when using `ReplicatedQueueManager`. + +The provided implementation, `ReactiveMessagingReplicationStrategy`, uses MicroProfile Reactive Messaging with message brokers like Apache Kafka. + +## Event Flow + +1. An event is generated in the A2A system (e.g., `TaskStatusUpdateEvent`) and enqueued locally +2. A `ReplicationHook` intercepts the event and sends it to the replication strategy +3. The replication strategy serializes the event and sends it to the message broker +4. Other A2A instances receive the event from the broker +5. The received event is deserialized and enqueued in each instance's local queue + +### Replicated Event Types + +The system replicates these event types while preserving their specific types: + +- `TaskStatusUpdateEvent` — task state changes (SUBMITTED, COMPLETED, etc.) +- `TaskArtifactUpdateEvent` — task artifact changes +- `Message` — chat messages and responses +- `Task` — complete task objects +- `A2AError` — error events + +Events are serialized using Jackson with polymorphic type information: + +```json +{ + "taskId": "task-123", + "event": { + "@type": "TaskStatusUpdateEvent", + "taskId": "task-123", + "status": { + "state": "completed", + "timestamp": "2023-09-29T10:30:00Z" + }, + "final": true, + "kind": "status-update" + } +} +``` + +## Quick Start + +### 1. Add Dependencies + +```xml + + + org.a2aproject.sdk + a2a-java-queue-manager-replicated-core + + + + + org.a2aproject.sdk + a2a-java-queue-manager-replication-mp-reactive + +``` + +### 2. Configure Kafka + +Add to your `application.properties`: + +```properties +# Outgoing (QueueManager -> Kafka) +mp.messaging.outgoing.replicated-events-out.connector=smallrye-kafka +mp.messaging.outgoing.replicated-events-out.topic=replicated-events +mp.messaging.outgoing.replicated-events-out.value.serializer=org.apache.kafka.common.serialization.StringSerializer + +# Incoming (Kafka -> QueueManager) +mp.messaging.incoming.replicated-events-in.connector=smallrye-kafka +mp.messaging.incoming.replicated-events-in.topic=replicated-events +mp.messaging.incoming.replicated-events-in.value.deserializer=org.apache.kafka.common.serialization.StringDeserializer +``` + +The channel names `replicated-events-in` and `replicated-events-out` correspond to the `@Incoming` and `@Channel` annotations in the `ReactiveMessagingReplicationStrategy`. + +### 3. Create the Kafka Topic + +```bash +kafka-topics.sh --create --topic replicated-events \ + --bootstrap-server localhost:9092 \ + --partitions 3 --replication-factor 1 +``` + +## Configuration + +### Kafka Settings + +```properties +# Kafka broker configuration +kafka.bootstrap.servers=kafka-broker-1:9092,kafka-broker-2:9092 + +# Topic configuration +mp.messaging.outgoing.replicated-events-out.topic=my-replicated-events +mp.messaging.incoming.replicated-events-in.topic=my-replicated-events + +# Consumer group (all A2A instances should share the same group) +mp.messaging.incoming.replicated-events-in.group.id=a2a-instance-group + +# Consumer behavior +mp.messaging.incoming.replicated-events-in.auto.offset.reset=earliest + +# Reliability configuration +mp.messaging.outgoing.replicated-events-out.acks=all +mp.messaging.outgoing.replicated-events-out.retries=3 + +# Performance tuning +mp.messaging.outgoing.replicated-events-out.batch.size=16384 +mp.messaging.incoming.replicated-events-in.max.poll.records=500 + +# Serialization (key serialization needed for partition-by-taskId strategy) +mp.messaging.outgoing.replicated-events-out.key.serializer=org.apache.kafka.common.serialization.StringSerializer +mp.messaging.incoming.replicated-events-in.key.deserializer=org.apache.kafka.common.serialization.StringDeserializer +``` + +## Production Considerations + +### Kafka Partitioning Strategy + +How you partition your Kafka topic significantly impacts scalability and correctness. + +#### Single Partition + +```bash +kafka-topics.sh --create --topic replicated-events \ + --bootstrap-server localhost:9092 \ + --partitions 1 --replication-factor 1 +``` + +- Guarantees global event ordering +- Suitable for development, testing, and low-throughput production (<1000 events/sec) +- Cannot parallelize consumption + +#### Partition by Task ID (Recommended) + +```bash +kafka-topics.sh --create --topic replicated-events \ + --bootstrap-server localhost:9092 \ + --partitions 10 --replication-factor 3 +``` + +The `ReactiveMessagingReplicationStrategy` sends the `taskId` as the Kafka message key, so Kafka automatically partitions by task ID. + +- **Per-task ordering guarantee**: all events for a single task go to the same partition +- **Horizontal scalability**: different tasks can be processed in parallel across partitions +- **Consumer parallelism**: multiple consumer instances can process different partitions concurrently +- **Rule of thumb**: number of partitions >= number of A2A instances + +### Consumer Group Configuration + +When using multiple partitions, ensure all A2A instances belong to the same consumer group: + +```properties +mp.messaging.incoming.replicated-events-in.group.id=a2a-instance-group +``` + +This ensures each partition is consumed by exactly one instance, and events for the same task always reach the same instance. + +### Poison Pill Cleanup + +When a task reaches a final state (COMPLETED, FAILED, CANCELED), all nodes must terminate their event consumers for that task. This is handled automatically via a `QueueClosedEvent` (poison pill) that is replicated to all nodes. + +The mechanism uses **transaction-aware CDI events**: the poison pill is only sent AFTER the final task state is committed to the database (`@Observes(during = TransactionPhase.AFTER_SUCCESS)`), eliminating race conditions. + +```java +// JpaDatabaseTaskStore fires CDI event after persist +if (task.status().state().isFinal()) { + taskFinalizedEvent.fire(new TaskFinalizedEvent(task.id(), task)); +} + +// ReplicatedQueueManager observes it after transaction commit +public void onTaskFinalized( + @Observes(during = TransactionPhase.AFTER_SUCCESS) TaskFinalizedEvent event) { + QueueClosedEvent closedEvent = new QueueClosedEvent(event.getTaskId()); + replicationStrategy.send(event.getTaskId(), closedEvent); +} +``` + +No configuration is required — the poison pill mechanism works automatically when using `JpaDatabaseTaskStore` together with `ReplicatedQueueManager`. + +### Grace Period + +A configurable grace period prevents queue recreation for tasks that were recently finalized, accommodating Kafka consumer lag and network delays: + +```properties +# Grace period for handling late-arriving events (default: 15 seconds) +a2a.replication.grace-period-seconds=15 +``` + +When a replicated event arrives after a task is finalized, the system checks if the task is still within the grace period before creating a new queue. Increase the grace period in high-latency networks; decrease it for systems with low latency and high task turnover. + +## Alternative Message Brokers + +While Kafka is the primary tested broker, MicroProfile Reactive Messaging supports other brokers: + +### Apache Pulsar + +```properties +mp.messaging.outgoing.replicated-events-out.connector=smallrye-pulsar +mp.messaging.incoming.replicated-events-in.connector=smallrye-pulsar +pulsar.client.serviceUrl=pulsar://localhost:6650 +``` + +### AMQP (RabbitMQ, etc.) + +```properties +mp.messaging.outgoing.replicated-events-out.connector=smallrye-amqp +mp.messaging.incoming.replicated-events-in.connector=smallrye-amqp +amqp-host=localhost +amqp-port=5672 +``` + +> **Note:** Alternative message brokers have not been tested in this project yet. + +### WildFly/Jakarta EE Servers + +For non-Quarkus environments, configure MicroProfile Reactive Messaging according to your application server's documentation. + +## Custom Replication Strategies + +You can implement your own `ReplicationStrategy` if MicroProfile Reactive Messaging does not fit your environment: + +```java +@ApplicationScoped +public class CustomReplicationStrategy implements ReplicationStrategy { + + @Override + public void send(String taskId, Event event) { + // Your replication logic (e.g., database, REST API, etc.) + } +} +``` + +## Monitoring + +### Logging + +Enable debug logging to monitor replication activity: + +```properties +quarkus.log.category."org.a2aproject.sdk.extras.queuemanager.replicated".level=DEBUG +quarkus.log.category."org.a2aproject.sdk.extras.taskstore.database.jpa".level=DEBUG +``` + +Example log entries: + +``` +Task abc-123 is in final state, firing TaskFinalizedEvent +Task abc-123 finalized - sending poison pill (QueueClosedEvent) after transaction commit +Skipping replicated event for inactive task abc-123 +``` + +### Health Checks + +When using Quarkus, the module integrates with MicroProfile Health: + +```properties +quarkus.messaging.kafka.health.timeout=5s +``` + +### CDI Event Monitoring + +You can monitor replicated events by observing CDI events: + +```java +@ApplicationScoped +public class ReplicationMonitor { + + public void onReplicatedEvent(@Observes ReplicatedEventQueueItem replicatedEvent) { + LOGGER.info("Received replicated event for task: " + replicatedEvent.getTaskId()); + } +} +``` diff --git a/docs/content/dev/extras/storage.md b/docs/content/dev/extras/storage.md new file mode 100644 index 000000000..15140b67b --- /dev/null +++ b/docs/content/dev/extras/storage.md @@ -0,0 +1,163 @@ +--- +title: Storage & Persistence +description: JPA-backed TaskStore and PushNotificationConfigStore for the A2A Java SDK, providing database persistence for production and load-balanced deployments. +layout: page +--- + +# Storage & Persistence + +The default SDK uses in-memory stores that are lost on restart. For production deployments — especially load-balanced environments — replace them with JPA-backed stores that persist data to a relational database. + +Both JPA stores use the Jakarta Persistence API (JPA 3.0+), making them suitable for any JPA provider and Jakarta EE application server. They share the same persistence unit name (`a2a-java`) and database configuration, so they can share a single datasource. + +> **Note:** Stored objects are serialized to JSON according to the current A2A specification version. Future specification versions may change the format. These stores are intended for the operational lifetime of tasks, not long-term archival. If you wish to keep objects stored between protocol versions, you may have to implement migration of the stored data. + +## JPA Task Store + +Replaces `InMemoryTaskStore` with a database-backed implementation. The `JpaDatabaseTaskStore` is annotated to take precedence over the default — it is a drop-in replacement. + +### 1. Add Dependency + +```xml + + org.a2aproject.sdk + a2a-java-extras-task-store-database-jpa + +``` + +### 2. Configure Database + +#### For Quarkus Reference Servers + +Add to your `application.properties`: + +```properties +quarkus.datasource.db-kind=postgresql +quarkus.datasource.jdbc.url=jdbc:postgresql://localhost:5432/a2a_db +quarkus.datasource.username=your_username +quarkus.datasource.password=your_password +quarkus.hibernate-orm.database.generation=update +``` + +#### For WildFly/Jakarta EE Servers + +> **Note:** On a Jakarta EE application server, the JPA dependencies are provided by the runtime. Mark them with `provided` in your POM to avoid packaging conflicts. + +Create or update your `persistence.xml`: + +```xml + + + + java:jboss/datasources/A2ADataSource + + org.a2aproject.sdk.extras.taskstore.database.jpa.JpaTask + true + + + + + + + +``` + +### 3. Database Schema + +The module automatically creates the required table: + +```sql +CREATE TABLE a2a_tasks ( + task_id VARCHAR(255) PRIMARY KEY, + context_id VARCHAR(255), + state VARCHAR(255), + status_timestamp TIMESTAMP, + task_data TEXT NOT NULL, + finalized_at TIMESTAMP +); +``` + +### Persistence Unit Name + +The module uses the persistence unit name `"a2a-java"`. Ensure your `persistence.xml` defines a persistence unit with this name. + +## JPA Push Notification Config Store + +Replaces `InMemoryPushNotificationConfigStore` with a database-backed implementation, ensuring push notification subscriptions survive application restarts. The `JpaDatabasePushNotificationConfigStore` is annotated to take precedence over the default — it is a drop-in replacement. + +### 1. Add Dependency + +```xml + + org.a2aproject.sdk + a2a-java-extras-push-notification-config-store-database-jpa + +``` + +### 2. Configure Database + +Uses the same datasource and persistence unit (`a2a-java`) as the JPA Task Store. + +#### For Quarkus Reference Servers + +The same `application.properties` configuration as the JPA Task Store applies. + +#### For WildFly/Jakarta EE Servers + +Create or update your `persistence.xml`: + +```xml + + + + java:jboss/datasources/A2ADataSource + + org.a2aproject.sdk.extras.pushnotificationconfigstore.database.jpa.JpaPushNotificationConfig + true + + + + + + + +``` + +### 3. Database Schema + +The module automatically creates the required table with a composite primary key: + +```sql +CREATE TABLE a2a_push_notification_configs ( + task_id VARCHAR(255) NOT NULL, + config_id VARCHAR(255) NOT NULL, + task_data TEXT NOT NULL, + protocol_version VARCHAR(255), + created_at TIMESTAMP, + PRIMARY KEY (task_id, config_id) +); +``` + +## Using Both Stores Together + +When using both JPA stores, combine their entity classes in a single `persistence.xml`: + +```xml + + + + java:jboss/datasources/A2ADataSource + + org.a2aproject.sdk.extras.taskstore.database.jpa.JpaTask + org.a2aproject.sdk.extras.pushnotificationconfigstore.database.jpa.JpaPushNotificationConfig + true + + + + + + + +``` + +For Quarkus, no additional configuration is needed — both entities are discovered automatically. diff --git a/docs/content/dev/getting-started.md b/docs/content/dev/getting-started.md new file mode 100644 index 000000000..7cac5d145 --- /dev/null +++ b/docs/content/dev/getting-started.md @@ -0,0 +1,46 @@ +--- +title: Getting Started +description: Get started with the A2A Java SDK — overview, guides, and next steps. +layout: page +--- + +# Getting Started + +The A2A Java SDK is a multi-module Maven library for implementing the [Agent2Agent (A2A) Protocol](https://a2a-protocol.org/) in Java. It provides both client and server support for agent communication over JSON-RPC, gRPC, and REST transports. + +## What You Can Build + +- **A2A Servers** — Expose your Java agent as an A2A-compliant service that other agents and clients can discover and communicate with. +- **A2A Clients** — Connect to any A2A-compliant agent, with streaming, push notifications, and task management. + +## Quick Start + +Add the reference server dependency to your Maven project: + +```xml + + org.a2aproject.sdk + a2a-java-sdk-reference-jsonrpc + $\{org.a2aproject.sdk.version} + +``` + +Then implement an `AgentExecutor` and define an `AgentCard`. See the [Server Guide](server) for the full walkthrough. + +## Guides + +| Guide | Description | +|-------|-------------| +| [Server Guide](server) | Run your Java application as an A2A server | +| [Client Guide](client) | Communicate with A2A-compliant agents | +| [Configuration](configuration) | Config properties, MicroProfile Config, custom providers | +| [Task Authorization](authorization) | Per-user access control for multi-user deployments | +| [Backward Compatibility](compatibility) | Serve v1.0 and v0.3 clients simultaneously | +| [Extras](extras) | Optional add-ons: database storage, OpenTelemetry, HTTP clients | +| [BOMs](boms) | Dependency management with Bill of Materials | +| [Examples](examples) | Hello World walkthroughs and sample applications | + +## Requirements + +- Java 17+ +- Maven 3.8+ diff --git a/docs/content/dev/server.md b/docs/content/dev/server.md new file mode 100644 index 000000000..39fe727dd --- /dev/null +++ b/docs/content/dev/server.md @@ -0,0 +1,170 @@ +--- +title: A2A Server Guide +description: Run your agentic Java application as an A2A server following the Agent2Agent Protocol. +layout: page +--- + +# 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, follow the steps below. + +## Supported Transports + +- JSON-RPC 2.0 +- gRPC +- HTTP+JSON/REST + +## 1. Add a Server Dependency + +### JSON-RPC + +```xml + + org.a2aproject.sdk + a2a-java-sdk-reference-jsonrpc + + $\{org.a2aproject.sdk.version} + +``` + +### gRPC + +```xml + + org.a2aproject.sdk + a2a-java-sdk-reference-grpc + $\{org.a2aproject.sdk.version} + +``` + +### HTTP+JSON/REST + +```xml + + org.a2aproject.sdk + a2a-java-sdk-reference-rest + $\{org.a2aproject.sdk.version} + +``` + +You can add more than one transport dependency to support multiple protocols simultaneously. + +## 2. Define an Agent Card + +```java +@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. Implement an Agent Executor + +```java +@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 { + if (context.getTask() == null) { + agentEmitter.submit(); + } + agentEmitter.startWork(); + + String userMessage = extractTextFromMessage(context.getMessage()); + String response = weatherAgent.chat(userMessage); + + agentEmitter.addArtifact(List.of(new TextPart(response))); + agentEmitter.complete(); + } + + @Override + public void cancel(RequestContext context, AgentEmitter agentEmitter) throws JSONRPCError { + Task task = context.getTask(); + if (task == null) { + agentEmitter.cancel(); + return; + } + if (task.status().state() == TaskState.CANCELED || + task.status().state() == TaskState.COMPLETED) { + throw new TaskNotCancelableError(); + } + agentEmitter.cancel(); + } + + private String extractTextFromMessage(Message message) { + if (message == null) { + return ""; + } + StringBuilder textBuilder = new StringBuilder(); + for (Part part : message.parts()) { + if (part instanceof TextPart textPart) { + textBuilder.append(textPart.text()); + } + } + return textBuilder.toString(); + } + } +} +``` + +## 4. Configuration + +See [Configuration](configuration) for all config properties and tuning. + +## 5. Task Authorization (Optional) + +See [Task Authorization](authorization) for per-user access control. + +## Backward Compatibility with v0.3 + +See [Backward Compatibility](compatibility) for multi-version modules, version routing, and v0.3 client support. + +## Server Integrations + +- **Quarkus** — Reference implementations are Quarkus-based (JSON-RPC, gRPC, REST) +- **Jakarta EE** — [a2a-jakarta](https://github.com/wildfly-extras/a2a-jakarta) works with any Jakarta EE Web Profile runtime + +See [CONTRIBUTING_INTEGRATIONS.md](https://github.com/a2aproject/a2a-java/blob/main/CONTRIBUTING_INTEGRATIONS.md) to submit your own integration. diff --git a/docs/content/index.html b/docs/content/index.html index ebd358091..569d1ecae 100644 --- a/docs/content/index.html +++ b/docs/content/index.html @@ -13,8 +13,8 @@ {#title}A2A Java SDK{/title} {#tagline}Implement the Agent2Agent Protocol in Java{/tagline} {#subtitle}A multi-module Maven library providing client and server support for A2A agent communication over JSON-RPC, gRPC, and REST transports.{/subtitle} - Server Guide - Client Guide + {#for v in cdi:versions.list}{#if v.defaultVersion}Server Guide + Client Guide {/if}{/for} {/}
@@ -42,7 +42,7 @@

Quick Install

<version>$\{org.a2aproject.sdk.version}</version> </dependency> -

See the Getting Started guide, or jump directly to the Server Guide and Client Guide for full setup instructions.

+ {#for v in cdi:versions.list}{#if v.defaultVersion}

See the Getting Started guide, or jump directly to the Server Guide and Client Guide for full setup instructions.

{/if}{/for}
diff --git a/docs/content/posts/2026-06-29-a2a-java-sdk-1-1-0-final-released/index.md b/docs/content/posts/2026-06-29-a2a-java-sdk-1-1-0-final-released/index.md index 313e0a34e..c42e9f064 100644 --- a/docs/content/posts/2026-06-29-a2a-java-sdk-1-1-0-final-released/index.md +++ b/docs/content/posts/2026-06-29-a2a-java-sdk-1-1-0-final-released/index.md @@ -40,7 +40,7 @@ public class MyTaskAuthorizationProvider implements TaskAuthorizationProvider { @Override public boolean isTaskRecorded(String taskId) { - return ownershipStore.contains(taskId); + return ownershipStore.containsKey(taskId); } @Override diff --git a/docs/data/menu.yml b/docs/data/menu.yml index eae08498f..3c3b37424 100644 --- a/docs/data/menu.yml +++ b/docs/data/menu.yml @@ -2,63 +2,31 @@ items: - title: "Home" path: "/" icon: "fa-solid fa-house" + position: "top" - title: "Announcements" path: "/announces" icon: "fa-regular fa-newspaper" - - title: "Getting Started" - path: "/getting-started" - icon: "fa-solid fa-rocket" - - title: "Documentation" - type: "group" - icon: "fa-solid fa-book" - - title: "Server" - path: "/server" - icon: "fa-solid fa-server" - group: "documentation" - - title: "Client" - path: "/client" - icon: "fa-solid fa-plug" - group: "documentation" - - title: "Configuration" - path: "/configuration" - icon: "fa-solid fa-sliders" - group: "documentation" - - title: "Authorization" - path: "/authorization" - icon: "fa-solid fa-shield-halved" - group: "documentation" - - title: "Compatibility" - path: "/compatibility" - icon: "fa-solid fa-code-branch" - group: "documentation" - - title: "Extras" - path: "/extras" - icon: "fa-solid fa-puzzle-piece" - group: "documentation" - - title: "BOMs" - path: "/boms" - icon: "fa-solid fa-cubes" - group: "documentation" - - title: "Examples" - path: "/examples" - icon: "fa-solid fa-flask" - group: "documentation" + position: "bottom" - title: "Javadoc" path: "https://javadoc.io/doc/org.a2aproject.sdk" icon: "fa-solid fa-file-code" - group: "documentation" target: "_blank" + position: "bottom" - title: "Community" path: "/community" icon: "fa-solid fa-users" + position: "bottom" - title: "Contributing" path: "/contributing" icon: "fa-solid fa-code-pull-request" + position: "bottom" - title: "A2A Protocol" path: "https://a2a-protocol.org/latest/" icon: "fa-solid fa-arrow-up-right-from-square" target: "_blank" + position: "bottom" - title: "GitHub" path: "https://github.com/a2aproject/a2a-java" icon: "fa-brands fa-github" target: "_blank" + position: "bottom" diff --git a/docs/data/versions/1.0.0.Final.yml b/docs/data/versions/1.0.0.Final.yml new file mode 100644 index 000000000..835438819 --- /dev/null +++ b/docs/data/versions/1.0.0.Final.yml @@ -0,0 +1,30 @@ +label: "1.0.0.Final" +path: "1.0.0.Final" +sortOrder: 1 +defaultVersion: false +devVersion: false +menu: + - title: "Getting Started" + path: "/getting-started" + icon: "fa-solid fa-rocket" + - title: "Server" + path: "/server" + icon: "fa-solid fa-server" + - title: "Client" + path: "/client" + icon: "fa-solid fa-plug" + - title: "Configuration" + path: "/configuration" + icon: "fa-solid fa-sliders" + - title: "Compatibility" + path: "/compatibility" + icon: "fa-solid fa-code-branch" + - title: "Extras" + path: "/extras" + icon: "fa-solid fa-puzzle-piece" + - title: "BOMs" + path: "/boms" + icon: "fa-solid fa-cubes" + - title: "Examples" + path: "/examples" + icon: "fa-solid fa-flask" diff --git a/docs/data/versions/1.1.0.Final.yml b/docs/data/versions/1.1.0.Final.yml new file mode 100644 index 000000000..c599f0d7e --- /dev/null +++ b/docs/data/versions/1.1.0.Final.yml @@ -0,0 +1,33 @@ +label: "1.1.0.Final" +path: "1.1.0.Final" +sortOrder: 2 +defaultVersion: true +devVersion: false +menu: + - title: "Getting Started" + path: "/getting-started" + icon: "fa-solid fa-rocket" + - title: "Server" + path: "/server" + icon: "fa-solid fa-server" + - title: "Client" + path: "/client" + icon: "fa-solid fa-plug" + - title: "Configuration" + path: "/configuration" + icon: "fa-solid fa-sliders" + - title: "Authorization" + path: "/authorization" + icon: "fa-solid fa-shield-halved" + - title: "Compatibility" + path: "/compatibility" + icon: "fa-solid fa-code-branch" + - title: "Extras" + path: "/extras" + icon: "fa-solid fa-puzzle-piece" + - title: "BOMs" + path: "/boms" + icon: "fa-solid fa-cubes" + - title: "Examples" + path: "/examples" + icon: "fa-solid fa-flask" diff --git a/docs/data/versions/dev.yml b/docs/data/versions/dev.yml new file mode 100644 index 000000000..d293ac3df --- /dev/null +++ b/docs/data/versions/dev.yml @@ -0,0 +1,33 @@ +label: "Development (unreleased)" +path: "dev" +sortOrder: 999 +defaultVersion: false +devVersion: true +menu: + - title: "Getting Started" + path: "/getting-started" + icon: "fa-solid fa-rocket" + - title: "Server" + path: "/server" + icon: "fa-solid fa-server" + - title: "Client" + path: "/client" + icon: "fa-solid fa-plug" + - title: "Configuration" + path: "/configuration" + icon: "fa-solid fa-sliders" + - title: "Authorization" + path: "/authorization" + icon: "fa-solid fa-shield-halved" + - title: "Compatibility" + path: "/compatibility" + icon: "fa-solid fa-code-branch" + - title: "Extras" + path: "/extras" + icon: "fa-solid fa-puzzle-piece" + - title: "BOMs" + path: "/boms" + icon: "fa-solid fa-cubes" + - title: "Examples" + path: "/examples" + icon: "fa-solid fa-flask" diff --git a/docs/src/main/java/org/a2aproject/docs/Versions.java b/docs/src/main/java/org/a2aproject/docs/Versions.java new file mode 100644 index 000000000..a94b04c6b --- /dev/null +++ b/docs/src/main/java/org/a2aproject/docs/Versions.java @@ -0,0 +1,56 @@ +package org.a2aproject.docs; + +import io.quarkiverse.roq.data.runtime.annotations.DataMapping; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +@DataMapping(value = "versions", type = DataMapping.Type.ARRAY_DIR) +public record Versions(List list) { + + public Versions { + if (list == null) { + throw new IllegalArgumentException("Versions list cannot be null"); + } + long defaultCount = list.stream().filter(Version::defaultVersion).count(); + if (defaultCount != 1) { + throw new IllegalStateException( + "Exactly one version must have defaultVersion: true, found " + defaultCount); + } + Set sortOrders = new HashSet<>(); + for (Version v : list) { + if (!sortOrders.add(v.sortOrder())) { + throw new IllegalStateException( + "Duplicate sortOrder " + v.sortOrder() + " in version " + v.label()); + } + } + } + + public record Version( + String label, + String path, + int sortOrder, + boolean defaultVersion, + boolean devVersion, + List menu) { + + public record MenuItem( + String title, + String path, + String icon) { + + public MenuItem { + if (title == null || title.isBlank()) { + throw new IllegalArgumentException("MenuItem title cannot be null or blank"); + } + if (path == null || path.isBlank()) { + throw new IllegalArgumentException("MenuItem path cannot be null or blank"); + } + if (!path.startsWith("/")) { + throw new IllegalStateException( + "MenuItem path must start with '/', got: " + path); + } + } + } + } +} diff --git a/docs/src/main/java/org/a2aproject/docs/package-info.java b/docs/src/main/java/org/a2aproject/docs/package-info.java new file mode 100644 index 000000000..1e2210fa0 --- /dev/null +++ b/docs/src/main/java/org/a2aproject/docs/package-info.java @@ -0,0 +1,2 @@ +@org.jspecify.annotations.NullMarked +package org.a2aproject.docs; diff --git a/docs/src/main/resources/templates/partials/roq-default/head-scripts.html b/docs/src/main/resources/templates/partials/roq-default/head-scripts.html index 33e803c07..fdd1b5069 100644 --- a/docs/src/main/resources/templates/partials/roq-default/head-scripts.html +++ b/docs/src/main/resources/templates/partials/roq-default/head-scripts.html @@ -1,5 +1,9 @@ {@java.lang.String tag} + + + + {#bundle /} {#search-script /} @@ -10,45 +14,150 @@
+{/let} diff --git a/docs/web/_custom.css b/docs/web/_custom.css index 9633de360..ac7dcfa80 100644 --- a/docs/web/_custom.css +++ b/docs/web/_custom.css @@ -1,8 +1,6 @@ /* Theme customization: https://iamroq.dev/theme/default/#css-customization */ /* Color palette inspired by https://a2a-protocol.org (Material for MkDocs, indigo/white) */ -@import url('https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,400;0,500;0,700;1,400&family=Roboto+Mono:wght@400;500&display=swap'); - @theme inline { /* Primary accent: Material Design Indigo */ --color-accent-50: #e8eaf6; @@ -45,45 +43,118 @@ @apply w-[200px]; } -/* ── Sidebar menu groups ──────────────────────────────────────────────────── */ +/* ── Version dropdown ────────────────────────────────────────────────────── */ + +.version-dropdown { + position: relative; + margin: 0.75rem 1rem; +} + +.version-dropdown-toggle { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + padding: 0.5rem 0.75rem; + background: rgba(255, 255, 255, 0.1); + border: 1px solid rgba(255, 255, 255, 0.2); + border-radius: 0.375rem; + color: inherit; + font-size: 0.875rem; + cursor: pointer; + transition: background 0.15s ease; +} -.menu-group-label a { - display: flex !important; - align-items: center !important; - flex-wrap: nowrap !important; - cursor: pointer !important; +.version-dropdown-toggle:hover { + background: rgba(255, 255, 255, 0.15); } -.menu-group-chevron { +.version-dropdown-chevron { font-size: 0.65rem; transition: transform 0.2s ease; - flex-shrink: 0; - margin-left: auto; + margin-left: 0.5rem; +} + +.version-dropdown-menu { + display: none; + position: absolute; + top: 100%; + left: 0; + right: 0; + z-index: 100; + margin-top: 0.25rem; + padding: 0.25rem 0; + background: var(--color-accent-800); + border: 1px solid rgba(255, 255, 255, 0.2); + border-radius: 0.375rem; + list-style: none; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); } -.menu-group-children-li { - padding: 0; - margin: 0; - list-style: none; +.version-dropdown-menu.version-dropdown-open { + display: block; } -.menu-group-children-wrapper { - overflow: hidden; - max-height: 1000px; - transition: max-height 0.25s ease; - padding: 0; - margin: 0; - list-style: none; +.version-dropdown-menu li { + padding: 0.4rem 0.75rem; + font-size: 0.85rem; + cursor: pointer; + transition: background 0.1s ease; +} + +.version-dropdown-menu li:hover { + background: rgba(255, 255, 255, 0.1); } -.menu-group-children-wrapper.menu-group-collapsed { - max-height: 0; +.version-badge { + display: inline-block; + font-size: 0.7rem; + padding: 0.1rem 0.35rem; + border-radius: 0.25rem; + background: rgba(255, 255, 255, 0.2); + margin-left: 0.35rem; + vertical-align: middle; +} + +.version-badge-dev { + background: rgba(255, 193, 7, 0.3); + color: #ffd54f; +} + +/* ── Dev banner ──────────────────────────────────────────────────────────── */ + +.dev-banner { + margin: 0.5rem 1rem; + padding: 0.5rem 0.75rem; + background: rgba(255, 193, 7, 0.15); + border: 1px solid rgba(255, 193, 7, 0.3); + border-radius: 0.375rem; + font-size: 0.8rem; + color: #ffd54f; +} + +.dev-banner a { + color: #fff; + text-decoration: underline; +} + +.dev-banner i { + margin-right: 0.35rem; +} + +/* ── Search version toggle ──────────────────────────────────────────────── */ + +.search-version-toggle { + display: flex; + align-items: center; + gap: 0.4rem; + margin-top: 0.5rem; + font-size: 0.8rem; + opacity: 0.8; } -.menu-group-child a { - padding-left: 2rem !important; - font-size: 0.9em; - opacity: 0.9; +.search-version-toggle input[type="checkbox"] { + accent-color: #5c6bc0; } /* The search overlay is inside .sidebar.main which is position:fixed on */