diff --git a/docs/content/about.md b/docs/content/about.md deleted file mode 100644 index b7ffc9c1a..000000000 --- a/docs/content/about.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -title: About -description: A static site generator built with Java and Quarkus. Zero config to get started, full power of the JVM when you need it. -layout: page ---- - -# About this site - -This site is built with [Roq](https://iamroq.dev), a static site generator powered by [Quarkus](https://quarkus.io). It combines the best of tools like Jekyll and Hugo with the Java ecosystem: zero configuration to get started, blazing fast live-reload in dev mode, and full access to Java when you need it. - -## Authors - -
- {#for id in cdi:authors.fields} - {#let author=cdi:authors.get(id)} - {#roq/authorCard name=author.name avatar=author.avatar?? nickname=author.nickname profile=author.profile /} - {/for} -
- diff --git a/docs/content/authorization.md b/docs/content/authorization.md new file mode 100644 index 000000000..10aa2e6d5 --- /dev/null +++ b/docs/content/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.contains(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/boms.md b/docs/content/boms.md new file mode 100644 index 000000000..e0dc6e1b4 --- /dev/null +++ b/docs/content/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/client.md b/docs/content/client.md index 94ca0492b..c39bb24f9 100644 --- a/docs/content/client.md +++ b/docs/content/client.md @@ -190,42 +190,8 @@ Client client = 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. +See [Backward Compatibility](compatibility#client-communicating-with-v03-agents) for using `Client_v0_3` with older protocol agents. ## Examples -- [Hello World Client](https://github.com/a2aproject/a2a-java/blob/main/examples/helloworld/client/README.md) — Java client talking to a Python A2A server -- [Hello World Server](https://github.com/a2aproject/a2a-java/blob/main/examples/helloworld/server/README.md) — Python client talking to a Java A2A server -- [a2a-samples repository](https://github.com/a2aproject/a2a-samples/tree/main/samples/java/agents) — More agent examples +See [Examples](examples) for Hello World walkthroughs and sample applications. diff --git a/docs/content/compatibility.md b/docs/content/compatibility.md new file mode 100644 index 000000000..aae404e3e --- /dev/null +++ b/docs/content/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/configuration.md b/docs/content/configuration.md new file mode 100644 index 000000000..304598fcb --- /dev/null +++ b/docs/content/configuration.md @@ -0,0 +1,147 @@ +--- +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 { + + @Inject + Environment env; // Your framework's config source (e.g. Spring Environment) + + @Inject + DefaultValuesConfigProvider defaultValues; + + @Override + public String getValue(String name) { + String value = env.getProperty(name); + if (value != null) { + return value; + } + return defaultValues.getValue(name); + } + + @Override + public Optional getOptionalValue(String name) { + String value = env.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/examples.md b/docs/content/examples.md new file mode 100644 index 000000000..90d3150e2 --- /dev/null +++ b/docs/content/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/google-a2a/a2a-samples) project: + +```bash +git clone https://github.com/google-a2a/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/google-a2a/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/getting-started.md b/docs/content/getting-started.md new file mode 100644 index 000000000..230a609dc --- /dev/null +++ b/docs/content/getting-started.md @@ -0,0 +1,47 @@ +--- +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 | +| [REST API Reference](rest-api-reference) | HTTP+JSON endpoint reference | +| [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/index.html b/docs/content/index.html index 4bf0db8b3..ebd358091 100644 --- a/docs/content/index.html +++ b/docs/content/index.html @@ -42,6 +42,36 @@

Quick Install

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

See the Server Guide and Client Guide for full setup instructions, or browse Community Articles for tutorials and examples.

+

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

+ + +
+

Compatibility

+ + + + + + + + + + + + + + + + + + + + + + + + + +
SDK VersionA2A ProtocolJava
1.0.0.Final1.0, 0.317+
1.1.0.Final1.0, 0.317+
dev (unreleased)1.0, 0.317+
diff --git a/docs/content/rest-api-reference.md b/docs/content/rest-api-reference.md new file mode 100644 index 000000000..6c48fc309 --- /dev/null +++ b/docs/content/rest-api-reference.md @@ -0,0 +1,327 @@ +--- +title: REST API Reference +description: HTTP+JSON endpoint reference for the A2A REST transport — request/response formats, SSE streaming, error handling. +layout: page +--- + +# REST API Reference + +REST transport implementation for the A2A Protocol, providing HTTP-based (`HTTP+JSON` protocol binding) communication between agents and clients. All request and response bodies use [Protobuf JSON](https://protobuf.dev/programming-guides/proto3/#json) serialization (camelCase field names). + +The `\{tenant}` path prefix is optional on all endpoints. When omitted, the extra slash is also omitted (e.g., `/message:send` instead of `/\{tenant}/message:send`). + +## Send Message + +Sends a message to the agent and blocks until the agent reaches a terminal or interrupted state, or returns immediately if `returnImmediately` is set. + +``` +POST /\{tenant}/message:send +Content-Type: application/json +``` + +**Request body** (`SendMessageRequest`): +```json +{ + "message": { + "messageId": "msg-1", + "role": "ROLE_USER", + "parts": [ + {"text": "Hello, what can you do?"} + ], + "contextId": "ctx-1" + }, + "configuration": { + "historyLength": 10, + "returnImmediately": false, + "acceptedOutputModes": ["text/plain"] + } +} +``` + +**Response** — one of: +- `{"task": { ... }}` — a `Task` object when the agent creates/updates a task +- `{"message": { ... }}` — a `Message` object when the agent replies without a task + +## Send Streaming Message + +Sends a message and streams task updates as Server-Sent Events (SSE). Requires `capabilities.streaming = true` in the agent card. + +``` +POST /\{tenant}/message:stream +Content-Type: application/json +Accept: text/event-stream +``` + +Request body is identical to `message:send`. Response is a stream of SSE events: + +``` +: SSE stream started + +id: 0 +data: {"statusUpdate":{"taskId":"task-1","contextId":"ctx-1","status":{"state":"TASK_STATE_WORKING"}}} + +id: 1 +data: {"artifactUpdate":{"taskId":"task-1","contextId":"ctx-1","artifact":{"artifactId":"a-1","parts":[{"text":"Hello!"}]}}} + +id: 2 +data: {"statusUpdate":{"taskId":"task-1","contextId":"ctx-1","status":{"state":"TASK_STATE_COMPLETED"}}} +``` + +Each `data` field contains a JSON-serialized `StreamResponse` with one of the following fields set: +- `task` — full `Task` snapshot +- `message` — a `Message` from the agent +- `statusUpdate` — a `TaskStatusUpdateEvent` +- `artifactUpdate` — a `TaskArtifactUpdateEvent` + +## Get Task + +Retrieves the current state of a task by ID. + +``` +GET /\{tenant}/tasks/\{taskId}?historyLength=10 +``` + +| Query parameter | Type | Description | +|-----------------|---------|-----------------------------------------------------------------------------| +| `historyLength` | integer | Maximum number of history messages to include. Omit for no limit; `0` for none. | + +**Response** — a `Task` object: +```json +{ + "id": "task-1", + "contextId": "ctx-1", + "status": { + "state": "TASK_STATE_COMPLETED", + "timestamp": "2023-10-27T10:00:00Z" + }, + "artifacts": [], + "history": [] +} +``` + +## List Tasks + +Lists tasks with optional filtering and pagination. + +``` +GET /\{tenant}/tasks +``` + +| Query parameter | Type | Description | +|------------------------|---------|--------------------------------------------------------------------------------------------| +| `contextId` | string | Filter by context ID. | +| `status` | string | Filter by task state. One of the `TaskState` enum values (e.g. `TASK_STATE_COMPLETED`). | +| `pageSize` | integer | Maximum number of tasks to return (server default: 50, max: 100). | +| `pageToken` | string | Pagination token from a previous `ListTasks` response. | +| `historyLength` | integer | Maximum history messages to include per task. | +| `statusTimestampAfter` | string | ISO-8601 timestamp. Only return tasks whose status was updated at or after this time. | +| `includeArtifacts` | boolean | Whether to include artifacts in results. Defaults to `false`. | + +**Response** (`ListTasksResponse`): +```json +{ + "tasks": [ ... ], + "nextPageToken": "", + "pageSize": 50, + "totalSize": 3 +} +``` + +### TaskState Values + +| Value | Description | +|-------------------------------|--------------------------------------------------| +| `TASK_STATE_SUBMITTED` | Task acknowledged, not yet processing. | +| `TASK_STATE_WORKING` | Task is actively being processed. | +| `TASK_STATE_COMPLETED` | Task finished successfully (terminal). | +| `TASK_STATE_FAILED` | Task finished with an error (terminal). | +| `TASK_STATE_CANCELED` | Task was canceled (terminal). | +| `TASK_STATE_REJECTED` | Agent declined to perform the task (terminal). | +| `TASK_STATE_INPUT_REQUIRED` | Agent needs additional input (interrupted). | +| `TASK_STATE_AUTH_REQUIRED` | Authentication is required to proceed (interrupted). | + +## Cancel Task + +Requests cancellation of a running task. The agent should transition the task to `TASK_STATE_CANCELED`. + +``` +POST /\{tenant}/tasks/\{taskId}:cancel +Content-Type: application/json +``` + +Request body is optional (may be empty or contain a `metadata` field). + +**Response** — the updated `Task` object on success. + +## Subscribe to Task + +Opens an SSE stream to receive real-time updates for an existing task. Requires `capabilities.streaming = true`. Returns `UnsupportedOperationError` if the task is already in a terminal state. + +``` +POST /\{tenant}/tasks/\{taskId}:subscribe +Accept: text/event-stream +``` + +Response is an SSE stream with the same event format as `message:stream`. + +## Push Notification Configs + +### Create Push Notification Config + +Creates a webhook configuration for push notifications on a task. + +``` +POST /\{tenant}/tasks/\{taskId}/pushNotificationConfigs +Content-Type: application/json +``` + +**Request body** (`TaskPushNotificationConfig`): +```json +{ + "url": "https://example.com/webhook", + "token": "optional-token", + "authentication": { + "scheme": "Bearer", + "credentials": "my-token" + } +} +``` + +**Response** — the created `TaskPushNotificationConfig` with its generated `id`. HTTP 201. + +### Get Push Notification Config + +``` +GET /\{tenant}/tasks/\{taskId}/pushNotificationConfigs/\{configId} +``` + +**Response** — the `TaskPushNotificationConfig` object. + +### List Push Notification Configs + +``` +GET /\{tenant}/tasks/\{taskId}/pushNotificationConfigs +``` + +| Query parameter | Type | Description | +|-----------------|---------|------------------------------------| +| `pageSize` | integer | Maximum configurations to return. | +| `pageToken` | string | Pagination token. | + +**Response** (`ListTaskPushNotificationConfigsResponse`): +```json +{ + "configs": [ ... ], + "nextPageToken": "" +} +``` + +### Delete Push Notification Config + +``` +DELETE /\{tenant}/tasks/\{taskId}/pushNotificationConfigs/\{configId} +``` + +**Response** — HTTP 204 No Content on success. + +## Get Agent Card + +Public discovery endpoint. Returns the agent's self-describing manifest. No authentication required. + +``` +GET /.well-known/agent-card.json +``` + +**Response** — an `AgentCard` object: +```json +{ + "name": "My Agent", + "description": "An example agent", + "version": "1.0.0", + "supportedInterfaces": [ ... ], + "capabilities": { + "streaming": true, + "pushNotifications": false + }, + "skills": [ ... ], + "defaultInputModes": ["text/plain"], + "defaultOutputModes": ["text/plain"] +} +``` + +## Get Extended Agent Card + +Returns additional agent metadata for authenticated clients. Requires `capabilities.extendedAgentCard = true` in the public agent card. + +``` +GET /\{tenant}/extendedAgentCard +``` + +**Response** — an `AgentCard` object (same structure as the public card, potentially with additional fields). + +## Request Headers + +| Header | Description | +|--------------------|-------------------------------------------------------------------------------------------------| +| `X-A2A-Version` | Requested A2A protocol version (e.g., `1.0`). Validated against the agent's supported versions. | +| `X-A2A-Extensions` | Comma-separated list of extension URIs the client supports. Required when the agent declares required extensions. | + +## Error Handling + +All error responses use [RFC 7807 Problem Details](https://tools.ietf.org/html/rfc7807) with `Content-Type: application/problem+json`. + +```json +{ + "type": "https://a2a-protocol.org/errors/task-not-found", + "title": "Task not found", + "status": 404, + "details": "" +} +``` + +| Field | Type | Description | +|-----------|---------|---------------------------------------------| +| `type` | string | URI identifying the error type. | +| `title` | string | Human-readable summary of the error. | +| `status` | integer | HTTP status code. | +| `details` | string | Additional error context (may be empty). | + +### Error Types + +| `type` URI | HTTP Status | Description | +|----------------------------------------------------------------|-------------|------------------------------------------------------------| +| `https://a2a-protocol.org/errors/task-not-found` | 404 | The requested task does not exist. | +| `https://a2a-protocol.org/errors/method-not-found` | 404 | The endpoint does not exist. | +| `https://a2a-protocol.org/errors/invalid-request` | 400 | Malformed request, missing required fields, or JSON parse error. | +| `https://a2a-protocol.org/errors/invalid-params` | 422 | Invalid parameter values (e.g., negative `historyLength`). | +| `https://a2a-protocol.org/errors/extension-support-required` | 400 | The agent requires an extension the client did not declare.| +| `https://a2a-protocol.org/errors/task-not-cancelable` | 409 | The task cannot be canceled in its current state. | +| `https://a2a-protocol.org/errors/content-type-not-supported` | 415 | The requested content type is not supported. | +| `https://a2a-protocol.org/errors/push-notification-not-supported` | 501 | Push notifications are not configured for this agent. | +| `https://a2a-protocol.org/errors/unsupported-operation` | 501 | The operation is not implemented or not applicable. | +| `https://a2a-protocol.org/errors/version-not-supported` | 400 | The requested protocol version is not supported. | +| `https://a2a-protocol.org/errors/invalid-agent-response` | 502 | The agent produced an invalid response. | +| `https://a2a-protocol.org/errors/extended-agent-card-not-configured` | 400 | The agent does not have an extended agent card configured. | +| `https://a2a-protocol.org/errors/internal-error` | 500 | An unexpected server-side error occurred. | + +## Client Integration + +The REST client (`client/transport/rest`) automatically maps error responses to typed A2A exceptions: + +```java +try { + Task task = client.getTask(new TaskQueryParams("task-123")); +} catch (A2AClientException e) { + if (e.getCause() instanceof TaskNotFoundError) { + // Handle task not found + } else if (e.getCause() instanceof UnsupportedOperationError) { + // Handle unsupported operation + } +} +``` + +## See Also + +- [A2A Protocol Specification](https://a2a-protocol.org/) +- [RFC 7807 Problem Details](https://tools.ietf.org/html/rfc7807) +- [Protobuf JSON Encoding](https://protobuf.dev/programming-guides/proto3/#json) diff --git a/docs/content/server.md b/docs/content/server.md index 315e8bd71..249200645 100644 --- a/docs/content/server.md +++ b/docs/content/server.md @@ -152,190 +152,15 @@ public class WeatherAgentExecutorProducer { ## 4. 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`](https://github.com/a2aproject/a2a-java/blob/main/integrations/microprofile-config/README.md) integration to override defaults via `application.properties`, environment variables, or system properties -- **Spring/other frameworks**: See the [integration module README](https://github.com/a2aproject/a2a-java/blob/main/integrations/microprofile-config/README.md#custom-config-providers) for how to implement a custom `A2AConfigProvider` -- **Reference implementations**: Already include the MicroProfile Config integration - -### Configuration Properties - -**Executor Settings** (Optional) - -The SDK uses a dedicated executor for async operations like streaming. Default: 5 core threads, 50 max threads. - -```properties -# Core thread pool size for the @Internal executor (default: 5) -a2a.executor.core-pool-size=5 - -# Maximum thread pool size (default: 50) -a2a.executor.max-pool-size=50 - -# Thread keep-alive time in seconds (default: 60) -a2a.executor.keep-alive-seconds=60 -``` - -**Blocking Call Timeouts** (Optional) - -```properties -# Timeout for agent execution in blocking calls (default: 30 seconds) -a2a.blocking.agent.timeout.seconds=30 - -# Timeout for event consumption in blocking calls (default: 5 seconds) -a2a.blocking.consumption.timeout.seconds=5 -``` - -**Why this matters:** -- **Streaming Performance**: The executor handles streaming subscriptions. Too few threads can cause timeouts under concurrent load. -- **Resource Management**: The dedicated executor prevents streaming operations from competing with the ForkJoinPool. -- **Concurrency**: In production with high concurrent streaming, increase pool sizes accordingly. -- **Agent Timeouts**: LLM-based agents may need longer timeouts (60–120s) compared to simple agents. - -**Note:** The reference server implementations (Quarkus-based) automatically include the MicroProfile Config integration, so properties work out of the box in `application.properties`. +See [Configuration](configuration) for all config properties and tuning. ## 5. Task Authorization (Optional) -> **⚠ Security note:** For multi-user deployments, a `TaskAuthorizationProvider` **must** be configured. Without one, all operations are permitted regardless of authentication — any authenticated user can read, modify, or cancel any task. Production deployments should use a fail-closed ownership policy (deny access when ownership is unknown). - -Implement `TaskAuthorizationProvider` to control per-user access: - -```java -@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().isAuthenticated(); - } - - @Override - public boolean isTaskRecorded(String taskId) { - return ownershipStore.contains(taskId); - } - - @Override - public void recordOwnership(ServerCallContext context, String taskId, TaskOperation op) { - ownershipStore.put(taskId, context.getUser().getUsername()); - } -} -``` - -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 | +See [Task Authorization](authorization) for per-user access control. ## 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. - -### 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} - -``` - -### 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. +See [Backward Compatibility](compatibility) for multi-version modules, version routing, and v0.3 client support. ## Server Integrations diff --git a/docs/data/menu.yml b/docs/data/menu.yml index 551048d95..61fe0d225 100644 --- a/docs/data/menu.yml +++ b/docs/data/menu.yml @@ -5,6 +5,9 @@ items: - 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" @@ -16,10 +19,34 @@ items: 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: "REST API" + path: "/rest-api-reference" + icon: "fa-solid fa-globe" + 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" - title: "Javadoc" path: "https://javadoc.io/doc/org.a2aproject.sdk" icon: "fa-solid fa-file-code" @@ -38,4 +65,4 @@ items: - title: "GitHub" path: "https://github.com/a2aproject/a2a-java" icon: "fa-brands fa-github" - target: "_blank" \ No newline at end of file + target: "_blank" diff --git a/docs/src/main/resources/templates/layouts/page.html b/docs/src/main/resources/templates/layouts/page.html new file mode 100644 index 000000000..2dd083319 --- /dev/null +++ b/docs/src/main/resources/templates/layouts/page.html @@ -0,0 +1,5 @@ +--- +theme-layout: page +link: /:raw-path +--- +{#insert /} diff --git a/docs/web/_custom.css b/docs/web/_custom.css index b662a727e..c801967ec 100644 --- a/docs/web/_custom.css +++ b/docs/web/_custom.css @@ -69,7 +69,7 @@ .menu-group-children-wrapper { overflow: hidden; - max-height: 300px; + max-height: 2000px; transition: max-height 0.25s ease; padding: 0; margin: 0; @@ -107,6 +107,35 @@ max-width: var(--container-5xl); } +/* ── Tables ──────────────────────────────────────────────────────────────── */ + +.page-content table, +.roq-section table { + width: 100%; + border-collapse: collapse; + margin: 1.5rem 0; +} + +.page-content th, +.page-content td, +.roq-section th, +.roq-section td { + padding: 0.6rem 1rem; + text-align: left; + border: 1px solid rgba(255, 255, 255, 0.15); +} + +.page-content th, +.roq-section th { + font-weight: 600; + background: rgba(255, 255, 255, 0.06); +} + +.page-content tr:nth-child(even), +.roq-section tr:nth-child(even) { + background: rgba(255, 255, 255, 0.03); +} + /* ── Quick Install section ────────────────────────────────────────────────── */ .roq-section pre {