diff --git a/.claude/architecture/compatibility_0.3.md b/.claude/architecture/compatibility_0.3.md
new file mode 100644
index 000000000..5316027f9
--- /dev/null
+++ b/.claude/architecture/compatibility_0.3.md
@@ -0,0 +1,544 @@
+# PRD: A2A Protocol 0.3 Backward Compatibility Layer
+
+> **Goal**: Allow the A2A Java SDK (v1.0) to interoperate with agents running protocol v0.3 through a dedicated compatibility layer.
+
+---
+
+## Motivation
+
+The A2A protocol evolved from v0.3 to v1.0 with significant breaking changes. Existing agents deployed with v0.3 cannot immediately upgrade. This compatibility layer enables a v1.0 SDK to communicate with v0.3 agents across all three transports (JSON-RPC, gRPC, REST) and allows v1.0 servers to accept v0.3 client requests.
+
+---
+
+## Scope
+
+### In Scope
+
+- Dedicated `compat-0.3` Maven module structure containing **only** 0.3-specific code
+- gRPC code generation from the v0.3 `a2a.proto`
+- Dedicated v0.3 client (`Client_v0_3`) exposing only features available in v0.3
+- Server-side conversion layer (`Convert_v0_3_To10RequestHandler`) that accepts v0.3 requests and delegates to v1.0 server-common
+- Server-side transport handlers for v0.3 (JSON-RPC, gRPC, REST)
+- Bidirectional mapping layer between v0.3 and v1.0 domain objects
+- Quarkus reference server implementations for v0.3
+- TCK conformance tests for v0.3
+- Integration test infrastructure (test-jar) for validating conversion layer
+- Inclusion in the SDK BOM as separate optional dependencies
+
+### Out of Scope
+
+- Changes to existing v1.0 modules (no regressions, no API changes)
+- Automatic protocol version detection (client must explicitly choose API version)
+- Extras modules (OpenTelemetry, JPA stores, etc.) for v0.3
+- v0.3 format agent card (v0.3 clients must be able to parse v1.0 agent card format)
+
+### Deferred
+
+- **Dual-version server deployment**: Running both v1.0 and v0.3 transports simultaneously in a single server instance with a unified agent card
+ - **Current state**: v0.3 reference servers work standalone; need integration pattern for dual deployment
+ - **Remaining work**: Define CDI qualifier pattern, path prefix strategy, and unified agent card production
+
+---
+
+## Breaking Changes: v0.3 → v1.0
+
+The compatibility layer bridges the following differences:
+
+### 1. Proto Package Namespace
+| Aspect | v0.3 | v1.0 |
+|--------|------|------|
+| Package | `a2a.v1` | `lf.a2a.v1` |
+
+### 2. RPC Method Changes
+| v0.3 | v1.0 | Change |
+|------|------|--------|
+| `TaskSubscription` | `SubscribeToTask` | Renamed |
+| `GetAgentCard` | `GetExtendedAgentCard` | Renamed |
+| `ListTaskPushNotificationConfig` | `ListTaskPushNotificationConfigs` | Pluralized |
+| `CreateTaskPushNotificationConfig(CreateTaskPushNotificationConfigRequest)` | `CreateTaskPushNotificationConfig(TaskPushNotificationConfig)` | Parameter type changed |
+| — | `ListTasks` | New in v1.0 (no v0.3 equivalent) |
+
+### 3. HTTP Endpoint Changes
+| v0.3 | v1.0 |
+|------|------|
+| `/v1/message:send` | `/message:send` (+ `/{tenant}/message:send`) |
+| `/v1/message:stream` | `/message:stream` (+ tenant) |
+| `/v1/{name=tasks/*}` | `/tasks/{id=*}` (+ tenant) |
+| `/v1/{name=tasks/*}:cancel` | `/tasks/{id=*}:cancel` (+ tenant) |
+| `/v1/{name=tasks/*}:subscribe` | `/tasks/{id=*}:subscribe` (+ tenant) |
+| `/v1/card` | `/extendedAgentCard` (+ tenant) |
+| `/v1/{parent=task/*/pushNotificationConfigs}` | `/tasks/{task_id=*}/pushNotificationConfigs` (+ tenant) |
+
+### 4. Configuration Field Changes
+| v0.3 `SendMessageConfiguration` | v1.0 `SendMessageConfiguration` |
+|----------------------------------|----------------------------------|
+| `push_notification` (PushNotificationConfig) | `task_push_notification_config` (TaskPushNotificationConfig) |
+| `blocking` (bool, default true) | `return_immediately` (bool, default false) — inverted semantics |
+| `history_length` (int32, 0 = unlimited) | `history_length` (optional int32, unset = no limit) |
+
+### 5. AgentCard / AgentInterface Changes
+| v0.3 | v1.0 |
+|------|------|
+| `url` + `preferred_transport` on AgentCard | Removed; replaced by `supported_interfaces` |
+| `additional_interfaces` | Folded into `supported_interfaces` |
+| No `tenant` field | `tenant` field added to AgentInterface |
+| `transport` field | Renamed to `protocol_binding` |
+
+### 6. Task State Naming
+| v0.3 | v1.0 |
+|------|------|
+| `TASK_STATE_CANCELLED` | `TASK_STATE_CANCELED` |
+
+### 7. Structural Changes
+- v1.0 removed the `kind` discriminator field from messages
+- v1.0 added `reference_task_ids` to `Message`
+- v1.0 added `TASK_STATE_REJECTED` enum value (no v0.3 equivalent)
+
+---
+
+## Design Decisions
+
+### Naming Convention: `_v0_3` Suffix
+
+All compat-0.3 classes use a `_v0_3` suffix to avoid naming conflicts with v1.0 classes and improve IDE navigation:
+
+- `Task_v0_3`, `AgentCard_v0_3`, `Client_v0_3`
+- `JSONRPCHandler_v0_3`, `GrpcHandler_v0_3`, `RestHandler_v0_3`
+- `Convert_v0_3_To10RequestHandler`, `ErrorConverter_v0_3`
+- Mappers: `TaskMapper_v0_3`, `MessageSendParamsMapper_v0_3`, etc.
+
+**Exception**: Generated gRPC classes use the package name `org.a2aproject.sdk.compat03.grpc` without suffix (controlled by proto `java_package` option).
+
+### Dedicated v0.3 Client
+
+The compat layer exposes a **dedicated `Client_v0_3`** that only provides features available in v0.3:
+
+- No `listTasks()` method (absent in v0.3)
+- Method names reflect v0.3 semantics where they differ
+- The client is a standalone API, not a wrapper around the v1.0 `Client`
+
+Users must explicitly check the `protocolVersion` field from the agent card and instantiate the correct client accordingly. No automatic version detection.
+
+### Server-Side Conversion Layer
+
+Instead of embedding conversion logic in each transport handler, the implementation uses a **dedicated conversion layer** that sits between v0.3 transport handlers and v1.0 server-common:
+
+```
+v0.3 Client Request
+ ↓
+v0.3 Transport Handler (JSONRPC/gRPC/REST)
+ ↓
+Convert_v0_3_To10RequestHandler
+ ↓ (converts v0.3 → v1.0)
+v1.0 DefaultRequestHandler
+ ↓
+AgentExecutor → AgentEmitter → MainEventBus
+ ↓
+v1.0 Response
+ ↓ (converts v1.0 → v0.3)
+Convert_v0_3_To10RequestHandler
+ ↓
+v0.3 Transport Handler
+ ↓
+v0.3 Client Response
+```
+
+**Benefits:**
+- **Single conversion point**: All v0.3↔v1.0 translation logic lives in `server-conversion` module
+- **Transport independence**: JSONRPC, gRPC, and REST handlers share identical conversion logic
+- **Testability**: Conversion layer can be tested independently of transport concerns
+- **Maintainability**: Changes to conversion rules require updates in one place only
+
+### TASK_STATE_REJECTED Handling
+
+`TASK_STATE_REJECTED` (v1.0-only) is mapped to `TASK_STATE_FAILED` when converting to v0.3 wire format. The original state is preserved in metadata (`"original_state": "REJECTED"`) so information is not entirely lost. Both are terminal states, so v0.3 clients can handle the result correctly.
+
+### Agent Card: v1.0 Only
+
+The agent card (`/.well-known/agent-card.json`) is produced only using the v1.0 format. The compat layer does not produce a v0.3-format agent card.
+
+A server that supports both versions should advertise this via a single v1.0 agent card containing multiple `AgentInterface` entries — one per version — each with its own URL and `protocolVersion` field. v0.3 clients must be able to parse the v1.0 agent card format to discover their endpoint.
+
+**Pros:**
+- **Single source of truth**: one agent card at one well-known URL describes all supported versions and transports
+- **Simpler server implementation**: no need for separate v0.3 agent card endpoint, serializer, or CDI producer
+- **Forward-looking**: encourages v0.3 clients to understand the v1.0 discovery format
+
+**Cons:**
+- **v0.3 clients must parse v1.0 agent card**: pure v0.3 clients that only understand the v0.3 structure need updating
+
+---
+
+## Module Structure
+
+All compatibility code lives under a top-level `compat-0.3/` directory:
+
+```
+compat-0.3/
+├── pom.xml # Parent POM for all compat-0.3 submodules
+├── spec/ # v0.3 spec types (POJOs)
+│ ├── pom.xml
+│ └── src/main/java/org/a2aproject/sdk/compat03/spec/
+│ ├── Task_v0_3.java
+│ ├── AgentCard_v0_3.java
+│ ├── Message_v0_3.java
+│ ├── A2AError_v0_3.java # Base error class
+│ ├── *Error_v0_3.java # Specific error types
+│ └── ...
+├── spec-grpc/ # v0.3 proto + generated classes
+│ ├── pom.xml
+│ └── src/main/
+│ ├── proto/a2a_v0_3.proto # v0.3 proto file (package a2a.v1)
+│ └── java/org/a2aproject/sdk/compat03/grpc/
+│ └── [generated classes] # No _v0_3 suffix (generated code)
+├── http-client/ # HTTP client abstraction for v0.3
+│ └── pom.xml
+├── server-conversion/ # ⭐ Core conversion layer (NEW)
+│ ├── pom.xml
+│ └── src/main/java/org/a2aproject/sdk/compat03/conversion/
+│ ├── Convert_v0_3_To10RequestHandler.java # Main adapter
+│ ├── ErrorConverter_v0_3.java # Error conversion
+│ └── mappers/
+│ ├── config/
+│ │ ├── A03ToV10MapperConfig.java # MapStruct config
+│ │ └── A2AMappers_v0_3.java # Mapper registry
+│ ├── params/ # Request param mappers
+│ │ ├── MessageSendParamsMapper_v0_3.java
+│ │ ├── TaskQueryParamsMapper_v0_3.java
+│ │ ├── CancelTaskParamsMapper_v0_3.java
+│ │ └── ...
+│ ├── domain/ # Domain object mappers
+│ │ ├── TaskMapper_v0_3.java
+│ │ ├── MessageMapper_v0_3.java
+│ │ ├── TaskStateMapper_v0_3.java
+│ │ ├── EventKindMapper_v0_3.java
+│ │ └── ...
+│ └── result/ # Response result mappers
+│ └── ListTaskPushNotificationConfigsResultMapper_v0_3.java
+├── client/ # v0.3-compatible client
+│ ├── base/ # Client_v0_3 — dedicated 0.3 API
+│ │ └── pom.xml
+│ └── transport/
+│ ├── spi/ # Transport SPI
+│ │ └── pom.xml
+│ ├── jsonrpc/ # JSON-RPC client transport for v0.3
+│ │ └── pom.xml
+│ ├── grpc/ # gRPC client transport for v0.3
+│ │ └── pom.xml
+│ └── rest/ # REST client transport for v0.3
+│ └── pom.xml
+├── transport/ # Server-side transport handlers for v0.3
+│ ├── jsonrpc/ # Accept v0.3 JSON-RPC requests
+│ │ └── pom.xml
+│ ├── grpc/ # Accept v0.3 gRPC requests
+│ │ └── pom.xml
+│ └── rest/ # Accept v0.3 REST requests
+│ └── pom.xml
+├── reference/ # Quarkus reference servers for v0.3
+│ ├── jsonrpc/ # Reference JSON-RPC server
+│ │ └── pom.xml
+│ ├── grpc/ # Reference gRPC server
+│ │ └── pom.xml
+│ └── rest/ # Reference REST server
+│ └── pom.xml
+└── tck/ # v0.3 conformance tests
+ └── pom.xml
+```
+
+### Java Package Convention
+
+All compat-0.3 code uses the `org.a2aproject.sdk.compat03` package root:
+
+- `org.a2aproject.sdk.compat03.spec` — v0.3 spec types
+- `org.a2aproject.sdk.compat03.grpc` — generated proto classes
+- `org.a2aproject.sdk.compat03.conversion` — conversion layer and mappers
+- `org.a2aproject.sdk.compat03.client` — dedicated v0.3 client API
+- `org.a2aproject.sdk.compat03.client.transport.{jsonrpc,grpc,rest}` — client transports
+- `org.a2aproject.sdk.compat03.transport.{jsonrpc,grpc,rest}` — server transports
+- `org.a2aproject.sdk.compat03.server.{apps,grpc,rest}.quarkus` — reference servers
+- `org.a2aproject.sdk.compat03.tck` — conformance tests
+
+**Note**: During this implementation, the main codebase was migrated from `io.github.a2asdk` (groupId) and `io.a2a` (package) to `org.a2aproject.sdk` (both groupId and package) via PRs #750 and #786.
+
+---
+
+## Conversion Layer Architecture
+
+### Core Component: `Convert_v0_3_To10RequestHandler`
+
+This is the central adapter that bridges v0.3 transport handlers and v1.0 server-common:
+
+**Responsibilities:**
+- Convert v0.3 params → v1.0 params using mappers
+- Delegate to v1.0 `RequestHandler`
+- Convert v1.0 results → v0.3 results
+- Handle streaming publishers with element-by-element conversion
+- Map method name differences (e.g., `onSetTaskPushNotificationConfig` → `onCreateTaskPushNotificationConfig`)
+
+**Location**: `compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/Convert_v0_3_To10RequestHandler.java`
+
+### Mapper Organization
+
+Mappers are organized by function using MapStruct:
+
+| Category | Purpose | Examples |
+|----------|---------|----------|
+| **params/** | Convert v0.3 request params → v1.0 | `MessageSendParamsMapper_v0_3`, `TaskQueryParamsMapper_v0_3` |
+| **domain/** | Convert core domain objects bidirectionally | `TaskMapper_v0_3`, `MessageMapper_v0_3`, `TaskStateMapper_v0_3` |
+| **result/** | Convert v1.0 results → v0.3 | `ListTaskPushNotificationConfigsResultMapper_v0_3` |
+
+**Key Mappings:**
+
+| v1.0 Type | v0.3 Type | Notes |
+|-----------|-----------|-------|
+| `SendMessageConfiguration` | `SendMessageConfiguration_v0_3` | `return_immediately` ↔ `!blocking`, `task_push_notification_config` ↔ `push_notification` |
+| `Task` | `Task_v0_3` | `CANCELED` ↔ `CANCELLED` |
+| `TaskState.TASK_STATE_REJECTED` | `TaskState_v0_3.TASK_STATE_FAILED` | Map to FAILED + metadata `"original_state": "REJECTED"` |
+| `Message` | `Message_v0_3` | Drop `reference_task_ids` for v0.3 |
+
+### Error Mapping
+
+The `ErrorConverter_v0_3` class centralizes error translation between v0.3 and v1.0:
+
+**v0.3 → v1.0 (receiving errors):**
+- Extract `code` and `message` from v0.3 `A2AError_v0_3`
+- Convert `data` (Object) to `details` (Map): if `data` is a Map, use directly; otherwise wrap as `{"data": value}`
+- Instantiate correct v1.0 error class using `A2AErrorCodes.fromCode(code)`
+
+**v1.0 → v0.3 (sending errors):**
+- Extract `code`, `message`, and `details` from v1.0 `A2AError`
+- Convert `details` (Map) to `data` (Object)
+- For v1.0-only error codes, produce generic error with same code/message
+
+**Location**: `compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/ErrorConverter_v0_3.java`
+
+---
+
+## Test Infrastructure
+
+### Test-JAR Pattern
+
+The `server-conversion` module produces a test-jar containing shared test infrastructure:
+
+**Exported Classes:**
+- `AbstractA2ARequestHandlerTest_v0_3` — Base test class with v1.0 backend setup
+- `AbstractA2AServerServerTest_v0_3` — Integration test base for reference servers
+- Test fixtures and utilities
+
+**Maven Configuration:**
+```xml
+
+ maven-jar-plugin
+
+
+
+ test-jar
+
+
+
+
+```
+
+**Consumers:**
+- `compat-0.3/transport/jsonrpc` — `JSONRPCHandlerTest_v0_3`
+- `compat-0.3/transport/grpc` — `GrpcHandlerTest_v0_3`
+- `compat-0.3/transport/rest` — `RestHandlerTest_v0_3`
+- `compat-0.3/reference/{jsonrpc,grpc,rest}` — Integration tests
+
+### Test Coverage
+
+✅ **Complete:**
+- Core transport handler tests (JSONRPC, gRPC, REST)
+- Streaming tests (Flow.Publisher, SSE, gRPC server streaming)
+- Error mapping tests
+- Task state conversion tests
+- Reference server integration tests
+
+🔲 **Deferred:**
+- Push notification tests (depends on TestHttpClient porting)
+- Test metadata classes (classpath scanning)
+
+---
+
+## User Experience
+
+### Client: Talking to a v0.3 Agent
+
+**1. Add the compat client dependency:**
+
+```xml
+
+ org.a2aproject.sdk
+ a2a-java-sdk-compat-0.3-client
+
+
+
+ org.a2aproject.sdk
+ a2a-java-sdk-compat-0.3-client-transport-jsonrpc
+
+```
+
+**2. Check the agent card and choose the right client:**
+
+```java
+AgentCard card = // ... fetch agent card from /.well-known/agent-card.json
+
+for (AgentInterface iface : card.supportedInterfaces()) {
+ if ("0.3".equals(iface.protocolVersion())) {
+ // Use the compat client for v0.3 agents
+ Client_v0_3 client = ClientBuilder_v0_3.forUrl(iface.url())
+ .transport("JSONRPC")
+ .build();
+ } else if ("1.0".equals(iface.protocolVersion())) {
+ // Use the standard client for v1.0 agents
+ Client client = ClientBuilder.forUrl(iface.url())
+ .transport("JSONRPC")
+ .build();
+ }
+}
+```
+
+**3. Use the v0.3 client API:**
+
+`Client_v0_3` exposes only operations available in v0.3. Return types are v0.3 `org.a2aproject.sdk.compat03.spec` domain objects.
+
+### Server: Serving v0.3 Clients
+
+A server operator that wants to accept v0.3 clients:
+
+**1. Add the compat Maven dependency:**
+
+```xml
+
+ org.a2aproject.sdk
+ a2a-java-sdk-compat-0.3-reference-jsonrpc
+
+```
+
+**2. Provide a v0.3 AgentCard:**
+
+```java
+@Produces @PublicAgentCard
+public AgentCard_v0_3 agentCard() {
+ return AgentCard_v0_3.builder()
+ .name("My Agent")
+ .url("http://localhost:8081")
+ .preferredTransport("JSONRPC")
+ // ... rest of agent card
+ .build();
+}
+```
+
+**3. No changes to AgentExecutor:**
+
+The existing `AgentExecutor` implementation works unchanged. The compat reference module registers v0.3 transport endpoints via Quarkus CDI auto-discovery and delegates to the same `AgentExecutor` through the v1.0 server pipeline.
+
+### Server: Serving Both v0.3 and v1.0 (Deferred)
+
+**Status**: Architecture is in place but dual-version deployment pattern is not yet defined.
+
+**Remaining work:**
+- Define CDI qualifier pattern to differentiate v0.3 and v1.0 beans
+- Establish path prefix strategy (e.g., `/v0.3` for compat endpoints)
+- Unified agent card production with multiple `AgentInterface` entries
+- Integration testing for dual-version scenarios
+
+**Recommended approach:**
+1. Use separate URLs per protocol version (e.g., `http://localhost:9999` for v1.0, `http://localhost:9999/v0.3` for v0.3)
+2. Declare both in the v1.0 agent card with different `protocolVersion` values
+3. Register both transport handlers via Quarkus CDI with path-based routing
+
+---
+
+## Implementation Summary
+
+The implementation was completed in phases:
+
+### Phase 1: Foundation
+- Set up module structure and POMs
+- Port v0.3 spec types to `compat-0.3/spec`
+- Generate v0.3 gRPC classes in `compat-0.3/spec-grpc`
+- Apply `_v0_3` suffix naming convention
+
+### Phase 2: Conversion Layer
+- Create `server-conversion` module
+- Implement `Convert_v0_3_To10RequestHandler`
+- Build MapStruct mappers (params, domain, result)
+- Centralize error conversion in `ErrorConverter_v0_3`
+- Export test infrastructure via test-jar
+
+### Phase 3: Transport Handlers
+- Implement server-side transport handlers (JSONRPC, gRPC, REST)
+- Wire each transport to `Convert_v0_3_To10RequestHandler`
+- Test with v1.0 backend via `AbstractA2ARequestHandlerTest_v0_3`
+
+### Phase 4: Client
+- Implement `Client_v0_3` API
+- Build client transports (JSONRPC, gRPC, REST)
+- Validate against v0.3 spec constraints
+
+### Phase 5: Reference Servers
+- Port Quarkus reference servers (JSONRPC, gRPC, REST)
+- Integrate with test infrastructure
+- Run integration tests using v0.3 client against v0.3 reference servers
+
+### Phase 6: Testing & Validation
+- Port transport handler tests (37+ tests across 3 transports)
+- Port streaming tests (Flow.Publisher, SSE, gRPC)
+- Port reference server tests (125+ integration tests passing)
+- Enable TCK module
+
+---
+
+## Key Differences from Original Plan
+
+1. **Conversion layer as separate module**: Original plan embedded mapping in `spec-grpc`; implementation uses dedicated `server-conversion` module with `Convert_v0_3_To10RequestHandler`
+
+2. **`_v0_3` suffix naming**: Not in original plan; adopted to eliminate IDE naming conflicts (233 out of 284 classes had conflicts)
+
+3. **No `reference/common` module**: Each reference server is standalone; no shared reference base
+
+4. **Test-jar pattern**: Test infrastructure exported from `server-conversion` module rather than reference common
+
+5. **Package migration during implementation**: The main codebase migrated from `io.a2a` to `org.a2aproject.sdk` while this work was in progress (PRs #750, #786)
+
+6. **Implementation order**: Test infrastructure built early; tests ported incrementally to validate conversion layer
+
+---
+
+## Testing Strategy
+
+| Component | Test Type | Coverage |
+|-----------|-----------|----------|
+| Mappers | Unit tests | Round-trip conversion for every mapped type; edge cases (missing fields, v1.0-only features, REJECTED→FAILED) |
+| `Convert_v0_3_To10RequestHandler` | Integration tests | Via transport handler tests using real v1.0 backend |
+| Transport handlers | Unit + Integration | Handler-level tests + end-to-end via reference servers |
+| Client transports | Unit tests | Mocked v0.3 endpoints |
+| `Client_v0_3` | Unit tests | API coverage, absence of v1.0-only methods |
+| Reference servers | Integration tests | Full request/response cycle with v0.3 client |
+| TCK | Conformance tests | Protocol conformance against v0.3 spec |
+
+---
+
+## Status
+
+✅ **Complete:**
+- Module structure and POMs
+- v0.3 spec types and gRPC generation
+- Server conversion layer with all mappers
+- Server-side transport handlers (JSONRPC, gRPC, REST)
+- Client API and transports
+- Reference servers (JSONRPC, gRPC, REST)
+- Test infrastructure (test-jar pattern)
+- Core integration tests (125+ passing)
+- TCK module enabled
+
+🔲 **Deferred:**
+- Dual v1.0/v0.3 server deployment pattern
+- Push notification test porting (requires TestHttpClient)
+- Test metadata classes (classpath scanning)
+
+🧹 **Nice-to-have cleanup:**
+- Replace FQNs with imports (97 occurrences in 34 files)
+- Unify AgentCard producers across reference modules
+- Remove obsolete TODOs
diff --git a/boms/extras/src/it/extras-usage-test/src/main/java/org/a2aproject/sdk/test/ExtrasBomVerifier.java b/boms/extras/src/it/extras-usage-test/src/main/java/org/a2aproject/sdk/test/ExtrasBomVerifier.java
index 8357ce447..c5b727633 100644
--- a/boms/extras/src/it/extras-usage-test/src/main/java/org/a2aproject/sdk/test/ExtrasBomVerifier.java
+++ b/boms/extras/src/it/extras-usage-test/src/main/java/org/a2aproject/sdk/test/ExtrasBomVerifier.java
@@ -17,6 +17,7 @@ public class ExtrasBomVerifier extends DynamicBomVerifier {
"tck/", // TCK test suite
"tests/", // Integration tests
"test-utils-docker/", // Test utilities for Docker-based tests
+ "compat-0.3/", // Compat 0.3 modules (part of SDK BOM, not extras BOM)
"extras/queue-manager-replicated/tests-multi-instance/", // Test harness applications
"extras/queue-manager-replicated/tests-single-instance/", // Test harness applications
"extras/opentelemetry/integration-tests/" // Test harness applications
diff --git a/boms/reference/src/it/reference-usage-test/src/main/java/org/a2aproject/sdk/test/ReferenceBomVerifier.java b/boms/reference/src/it/reference-usage-test/src/main/java/org/a2aproject/sdk/test/ReferenceBomVerifier.java
index 6b20bb50e..d25473846 100644
--- a/boms/reference/src/it/reference-usage-test/src/main/java/org/a2aproject/sdk/test/ReferenceBomVerifier.java
+++ b/boms/reference/src/it/reference-usage-test/src/main/java/org/a2aproject/sdk/test/ReferenceBomVerifier.java
@@ -16,7 +16,8 @@ public class ReferenceBomVerifier extends DynamicBomVerifier {
"examples/", // Example applications
"tck/", // TCK test suite
"tests/", // Integration tests
- "test-utils-docker/" // Test utilities for Docker-based tests
+ "test-utils-docker/", // Test utilities for Docker-based tests
+ "compat-0.3/" // Compat 0.3 modules (part of SDK BOM, not reference BOM)
// Note: reference/ is NOT in this list - we want to verify those classes load
);
diff --git a/boms/sdk/pom.xml b/boms/sdk/pom.xml
index d907539d9..0019ebc1d 100644
--- a/boms/sdk/pom.xml
+++ b/boms/sdk/pom.xml
@@ -108,6 +108,86 @@
${project.version}
+
+
+ ${project.groupId}
+ a2a-java-sdk-compat-0.3-spec
+ ${project.version}
+
+
+ ${project.groupId}
+ a2a-java-sdk-compat-0.3-spec-grpc
+ ${project.version}
+
+
+
+
+ ${project.groupId}
+ a2a-java-sdk-compat-0.3-http-client
+ ${project.version}
+
+
+
+
+ ${project.groupId}
+ a2a-java-sdk-compat-0.3-client
+ ${project.version}
+
+
+ ${project.groupId}
+ a2a-java-sdk-compat-0.3-client-transport-spi
+ ${project.version}
+
+
+ ${project.groupId}
+ a2a-java-sdk-compat-0.3-client-transport-jsonrpc
+ ${project.version}
+
+
+ ${project.groupId}
+ a2a-java-sdk-compat-0.3-client-transport-grpc
+ ${project.version}
+
+
+ ${project.groupId}
+ a2a-java-sdk-compat-0.3-client-transport-rest
+ ${project.version}
+
+
+
+
+ ${project.groupId}
+ a2a-java-sdk-compat-0.3-transport-jsonrpc
+ ${project.version}
+
+
+ ${project.groupId}
+ a2a-java-sdk-compat-0.3-transport-grpc
+ ${project.version}
+
+
+ ${project.groupId}
+ a2a-java-sdk-compat-0.3-transport-rest
+ ${project.version}
+
+
+
+
+ ${project.groupId}
+ a2a-java-sdk-compat-0.3-reference-jsonrpc
+ ${project.version}
+
+
+ ${project.groupId}
+ a2a-java-sdk-compat-0.3-reference-grpc
+ ${project.version}
+
+
+ ${project.groupId}
+ a2a-java-sdk-compat-0.3-reference-rest
+ ${project.version}
+
+
${project.groupId}
diff --git a/boms/sdk/src/it/sdk-usage-test/pom.xml b/boms/sdk/src/it/sdk-usage-test/pom.xml
index 5da56e93e..3d158598b 100644
--- a/boms/sdk/src/it/sdk-usage-test/pom.xml
+++ b/boms/sdk/src/it/sdk-usage-test/pom.xml
@@ -103,6 +103,72 @@
a2a-java-sdk-transport-rest
+
+
+ org.a2aproject.sdk
+ a2a-java-sdk-compat-0.3-spec
+
+
+ org.a2aproject.sdk
+ a2a-java-sdk-compat-0.3-spec-grpc
+
+
+
+
+ org.a2aproject.sdk
+ a2a-java-sdk-compat-0.3-http-client
+
+
+
+
+ org.a2aproject.sdk
+ a2a-java-sdk-compat-0.3-client
+
+
+ org.a2aproject.sdk
+ a2a-java-sdk-compat-0.3-client-transport-spi
+
+
+ org.a2aproject.sdk
+ a2a-java-sdk-compat-0.3-client-transport-jsonrpc
+
+
+ org.a2aproject.sdk
+ a2a-java-sdk-compat-0.3-client-transport-grpc
+
+
+ org.a2aproject.sdk
+ a2a-java-sdk-compat-0.3-client-transport-rest
+
+
+
+
+ org.a2aproject.sdk
+ a2a-java-sdk-compat-0.3-transport-jsonrpc
+
+
+ org.a2aproject.sdk
+ a2a-java-sdk-compat-0.3-transport-grpc
+
+
+ org.a2aproject.sdk
+ a2a-java-sdk-compat-0.3-transport-rest
+
+
+
+
+ org.a2aproject.sdk
+ a2a-java-sdk-compat-0.3-reference-jsonrpc
+
+
+ org.a2aproject.sdk
+ a2a-java-sdk-compat-0.3-reference-grpc
+
+
+ org.a2aproject.sdk
+ a2a-java-sdk-compat-0.3-reference-rest
+
+
org.slf4j
diff --git a/boms/sdk/src/it/sdk-usage-test/src/main/java/org/a2aproject/sdk/test/SdkBomVerifier.java b/boms/sdk/src/it/sdk-usage-test/src/main/java/org/a2aproject/sdk/test/SdkBomVerifier.java
index f69d70b85..7f0607b8d 100644
--- a/boms/sdk/src/it/sdk-usage-test/src/main/java/org/a2aproject/sdk/test/SdkBomVerifier.java
+++ b/boms/sdk/src/it/sdk-usage-test/src/main/java/org/a2aproject/sdk/test/SdkBomVerifier.java
@@ -15,6 +15,7 @@ public class SdkBomVerifier extends DynamicBomVerifier {
"boms/", // BOM test modules themselves
"examples/", // Example applications
"tck/", // TCK test suite
+ "compat-0.3/tck/", // Compat 0.3 TCK (not yet enabled)
"tests/", // Integration tests
"test-utils-docker/" // Test utilities for Docker-based tests
);
diff --git a/compat-0.3/client/base/pom.xml b/compat-0.3/client/base/pom.xml
new file mode 100644
index 000000000..6fa0036ff
--- /dev/null
+++ b/compat-0.3/client/base/pom.xml
@@ -0,0 +1,84 @@
+
+
+ 4.0.0
+
+
+ org.a2aproject.sdk
+ a2a-java-sdk-compat-0.3-parent
+ 1.0.0.Beta2-SNAPSHOT
+ ../..
+
+ a2a-java-sdk-compat-0.3-client
+
+ jar
+
+ Java SDK A2A Compat 0.3 Client
+ Java SDK for the Agent2Agent Protocol (A2A) - Client
+
+
+
+ ${project.groupId}
+ a2a-java-sdk-compat-0.3-http-client
+
+
+ ${project.groupId}
+ a2a-java-sdk-compat-0.3-client-transport-spi
+
+
+ ${project.groupId}
+ a2a-java-sdk-compat-0.3-client-transport-jsonrpc
+
+
+ ${project.groupId}
+ a2a-java-sdk-compat-0.3-client-transport-grpc
+ test
+
+
+ ${project.groupId}
+ a2a-java-sdk-compat-0.3-client-transport-rest
+ test
+
+
+ ${project.groupId}
+ a2a-java-sdk-common
+
+
+ ${project.groupId}
+ a2a-java-sdk-compat-0.3-spec
+
+
+ ${project.groupId}
+ a2a-java-sdk-compat-0.3-spec-grpc
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter-api
+ test
+
+
+
+ org.mock-server
+ mockserver-netty
+ test
+
+
+ org.slf4j
+ slf4j-jdk14
+ test
+
+
+ io.grpc
+ grpc-testing
+ test
+
+
+ io.grpc
+ grpc-inprocess
+ test
+
+
+
+
\ No newline at end of file
diff --git a/compat-0.3/client/base/src/main/java/org/a2aproject/sdk/compat03/A2A_v0_3.java b/compat-0.3/client/base/src/main/java/org/a2aproject/sdk/compat03/A2A_v0_3.java
new file mode 100644
index 000000000..2be60b7ad
--- /dev/null
+++ b/compat-0.3/client/base/src/main/java/org/a2aproject/sdk/compat03/A2A_v0_3.java
@@ -0,0 +1,188 @@
+package org.a2aproject.sdk.compat03;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import org.a2aproject.sdk.compat03.client.http.A2ACardResolver_v0_3;
+import org.a2aproject.sdk.compat03.client.http.A2AHttpClient_v0_3;
+import org.a2aproject.sdk.compat03.client.http.JdkA2AHttpClient_v0_3;
+import org.a2aproject.sdk.compat03.spec.A2AClientError_v0_3;
+import org.a2aproject.sdk.compat03.spec.A2AClientJSONError_v0_3;
+import org.a2aproject.sdk.compat03.spec.AgentCard_v0_3;
+import org.a2aproject.sdk.compat03.spec.Message_v0_3;
+import org.a2aproject.sdk.compat03.spec.Part_v0_3;
+import org.a2aproject.sdk.compat03.spec.TextPart_v0_3;
+
+
+/**
+ * Constants and utility methods related to the A2A protocol.
+ */
+public class A2A_v0_3 {
+
+ /**
+ * Convert the given text to a user message.
+ *
+ * @param text the message text
+ * @return the user message
+ */
+ public static Message_v0_3 toUserMessage(String text) {
+ return toMessage(text, Message_v0_3.Role.USER, null);
+ }
+
+ /**
+ * Convert the given text to a user message.
+ *
+ * @param text the message text
+ * @param messageId the message ID to use
+ * @return the user message
+ */
+ public static Message_v0_3 toUserMessage(String text, String messageId) {
+ return toMessage(text, Message_v0_3.Role.USER, messageId);
+ }
+
+ /**
+ * Convert the given text to an agent message.
+ *
+ * @param text the message text
+ * @return the agent message
+ */
+ public static Message_v0_3 toAgentMessage(String text) {
+ return toMessage(text, Message_v0_3.Role.AGENT, null);
+ }
+
+ /**
+ * Convert the given text to an agent message.
+ *
+ * @param text the message text
+ * @param messageId the message ID to use
+ * @return the agent message
+ */
+ public static Message_v0_3 toAgentMessage(String text, String messageId) {
+ return toMessage(text, Message_v0_3.Role.AGENT, messageId);
+ }
+
+ /**
+ * Create a user message with text content and optional context and task IDs.
+ *
+ * @param text the message text (required)
+ * @param contextId the context ID to use (optional)
+ * @param taskId the task ID to use (optional)
+ * @return the user message
+ */
+ public static Message_v0_3 createUserTextMessage(String text, String contextId, String taskId) {
+ return toMessage(text, Message_v0_3.Role.USER, null, contextId, taskId);
+ }
+
+ /**
+ * Create an agent message with text content and optional context and task IDs.
+ *
+ * @param text the message text (required)
+ * @param contextId the context ID to use (optional)
+ * @param taskId the task ID to use (optional)
+ * @return the agent message
+ */
+ public static Message_v0_3 createAgentTextMessage(String text, String contextId, String taskId) {
+ return toMessage(text, Message_v0_3.Role.AGENT, null, contextId, taskId);
+ }
+
+ /**
+ * Create an agent message with custom parts and optional context and task IDs.
+ *
+ * @param parts the message parts (required)
+ * @param contextId the context ID to use (optional)
+ * @param taskId the task ID to use (optional)
+ * @return the agent message
+ */
+ public static Message_v0_3 createAgentPartsMessage(List> parts, String contextId, String taskId) {
+ if (parts == null || parts.isEmpty()) {
+ throw new IllegalArgumentException("Parts cannot be null or empty");
+ }
+ return toMessage(parts, Message_v0_3.Role.AGENT, null, contextId, taskId);
+ }
+
+ private static Message_v0_3 toMessage(String text, Message_v0_3.Role role, String messageId) {
+ return toMessage(text, role, messageId, null, null);
+ }
+
+ private static Message_v0_3 toMessage(String text, Message_v0_3.Role role, String messageId, String contextId, String taskId) {
+ Message_v0_3.Builder messageBuilder = new Message_v0_3.Builder()
+ .role(role)
+ .parts(Collections.singletonList(new TextPart_v0_3(text)))
+ .contextId(contextId)
+ .taskId(taskId);
+ if (messageId != null) {
+ messageBuilder.messageId(messageId);
+ }
+ return messageBuilder.build();
+ }
+
+ private static Message_v0_3 toMessage(List> parts, Message_v0_3.Role role, String messageId, String contextId, String taskId) {
+ Message_v0_3.Builder messageBuilder = new Message_v0_3.Builder()
+ .role(role)
+ .parts(parts)
+ .contextId(contextId)
+ .taskId(taskId);
+ if (messageId != null) {
+ messageBuilder.messageId(messageId);
+ }
+ return messageBuilder.build();
+ }
+
+ /**
+ * Get the agent card for an A2A agent.
+ *
+ * @param agentUrl the base URL for the agent whose agent card we want to retrieve
+ * @return the agent card
+ * @throws A2AClientError_v0_3 If an HTTP error occurs fetching the card
+ * @throws A2AClientJSONError_v0_3 If the response body cannot be decoded as JSON or validated against the AgentCard schema
+ */
+ public static AgentCard_v0_3 getAgentCard(String agentUrl) throws A2AClientError_v0_3, A2AClientJSONError_v0_3 {
+ return getAgentCard(new JdkA2AHttpClient_v0_3(), agentUrl);
+ }
+
+ /**
+ * Get the agent card for an A2A agent.
+ *
+ * @param httpClient the http client to use
+ * @param agentUrl the base URL for the agent whose agent card we want to retrieve
+ * @return the agent card
+ * @throws A2AClientError_v0_3 If an HTTP error occurs fetching the card
+ * @throws A2AClientJSONError_v0_3 If the response body cannot be decoded as JSON or validated against the AgentCard schema
+ */
+ public static AgentCard_v0_3 getAgentCard(A2AHttpClient_v0_3 httpClient, String agentUrl) throws A2AClientError_v0_3, A2AClientJSONError_v0_3 {
+ return getAgentCard(httpClient, agentUrl, null, null);
+ }
+
+ /**
+ * Get the agent card for an A2A agent.
+ *
+ * @param agentUrl the base URL for the agent whose agent card we want to retrieve
+ * @param relativeCardPath optional path to the agent card endpoint relative to the base
+ * agent URL, defaults to ".well-known/agent-card.json"
+ * @param authHeaders the HTTP authentication headers to use
+ * @return the agent card
+ * @throws A2AClientError_v0_3 If an HTTP error occurs fetching the card
+ * @throws A2AClientJSONError_v0_3 If the response body cannot be decoded as JSON or validated against the AgentCard schema
+ */
+ public static AgentCard_v0_3 getAgentCard(String agentUrl, String relativeCardPath, Map authHeaders) throws A2AClientError_v0_3, A2AClientJSONError_v0_3 {
+ return getAgentCard(new JdkA2AHttpClient_v0_3(), agentUrl, relativeCardPath, authHeaders);
+ }
+
+ /**
+ * Get the agent card for an A2A agent.
+ *
+ * @param httpClient the http client to use
+ * @param agentUrl the base URL for the agent whose agent card we want to retrieve
+ * @param relativeCardPath optional path to the agent card endpoint relative to the base
+ * agent URL, defaults to ".well-known/agent-card.json"
+ * @param authHeaders the HTTP authentication headers to use
+ * @return the agent card
+ * @throws A2AClientError_v0_3 If an HTTP error occurs fetching the card
+ * @throws A2AClientJSONError_v0_3 If the response body cannot be decoded as JSON or validated against the AgentCard schema
+ */
+ public static AgentCard_v0_3 getAgentCard(A2AHttpClient_v0_3 httpClient, String agentUrl, String relativeCardPath, Map authHeaders) throws A2AClientError_v0_3, A2AClientJSONError_v0_3 {
+ A2ACardResolver_v0_3 resolver = new A2ACardResolver_v0_3(httpClient, agentUrl, relativeCardPath, authHeaders);
+ return resolver.getAgentCard();
+ }
+}
diff --git a/compat-0.3/client/base/src/main/java/org/a2aproject/sdk/compat03/client/AbstractClient_v0_3.java b/compat-0.3/client/base/src/main/java/org/a2aproject/sdk/compat03/client/AbstractClient_v0_3.java
new file mode 100644
index 000000000..d27372f69
--- /dev/null
+++ b/compat-0.3/client/base/src/main/java/org/a2aproject/sdk/compat03/client/AbstractClient_v0_3.java
@@ -0,0 +1,392 @@
+package org.a2aproject.sdk.compat03.client;
+
+import static org.a2aproject.sdk.util.Assert.checkNotNullParam;
+
+import java.util.List;
+import java.util.Map;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+
+import org.a2aproject.sdk.compat03.client.transport.spi.interceptors.ClientCallContext_v0_3;
+import org.a2aproject.sdk.compat03.spec.A2AClientException_v0_3;
+import org.a2aproject.sdk.compat03.spec.AgentCard_v0_3;
+import org.a2aproject.sdk.compat03.spec.DeleteTaskPushNotificationConfigParams_v0_3;
+import org.a2aproject.sdk.compat03.spec.GetTaskPushNotificationConfigParams_v0_3;
+import org.a2aproject.sdk.compat03.spec.ListTaskPushNotificationConfigParams_v0_3;
+import org.a2aproject.sdk.compat03.spec.Message_v0_3;
+import org.a2aproject.sdk.compat03.spec.PushNotificationConfig_v0_3;
+import org.a2aproject.sdk.compat03.spec.Task_v0_3;
+import org.a2aproject.sdk.compat03.spec.TaskIdParams_v0_3;
+import org.a2aproject.sdk.compat03.spec.TaskPushNotificationConfig_v0_3;
+import org.a2aproject.sdk.compat03.spec.TaskQueryParams_v0_3;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
+/**
+ * Abstract class representing an A2A client. Provides a standard set
+ * of methods for interacting with an A2A agent, regardless of the underlying
+ * transport protocol. It supports sending messages, managing tasks, and
+ * handling event streams.
+ */
+public abstract class AbstractClient_v0_3 {
+
+ private final List> consumers;
+ private final @Nullable Consumer streamingErrorHandler;
+
+ public AbstractClient_v0_3(List> consumers) {
+ this(consumers, null);
+ }
+
+ public AbstractClient_v0_3(@NonNull List> consumers, @Nullable Consumer streamingErrorHandler) {
+ checkNotNullParam("consumers", consumers);
+ this.consumers = consumers;
+ this.streamingErrorHandler = streamingErrorHandler;
+ }
+
+ /**
+ * Send a message to the remote agent. This method will automatically use
+ * the streaming or non-streaming approach as determined by the server's
+ * agent card and the client configuration. The configured client consumers
+ * will be used to handle messages, tasks, and update events received
+ * from the remote agent. The configured streaming error handler will be used
+ * if an error occurs during streaming. The configured client push notification
+ * configuration will get used for streaming.
+ *
+ * @param request the message
+ * @throws A2AClientException_v0_3 if sending the message fails for any reason
+ */
+ public void sendMessage(Message_v0_3 request) throws A2AClientException_v0_3 {
+ sendMessage(request, null);
+ }
+
+ /**
+ * Send a message to the remote agent. This method will automatically use
+ * the streaming or non-streaming approach as determined by the server's
+ * agent card and the client configuration. The configured client consumers
+ * will be used to handle messages, tasks, and update events received
+ * from the remote agent. The configured streaming error handler will be used
+ * if an error occurs during streaming. The configured client push notification
+ * configuration will get used for streaming.
+ *
+ * @param request the message
+ * @param context optional client call context for the request (may be {@code null})
+ * @throws A2AClientException_v0_3 if sending the message fails for any reason
+ */
+ public abstract void sendMessage(Message_v0_3 request, @Nullable ClientCallContext_v0_3 context) throws A2AClientException_v0_3;
+
+ /**
+ * Send a message to the remote agent. This method will automatically use
+ * the streaming or non-streaming approach as determined by the server's
+ * agent card and the client configuration. The specified client consumers
+ * will be used to handle messages, tasks, and update events received
+ * from the remote agent. The specified streaming error handler will be used
+ * if an error occurs during streaming. The configured client push notification
+ * configuration will get used for streaming.
+ *
+ * @param request the message
+ * @param consumers a list of consumers to pass responses from the remote agent to
+ * @param streamingErrorHandler an error handler that should be used for the streaming case if an error occurs
+ * @throws A2AClientException_v0_3 if sending the message fails for any reason
+ */
+ public void sendMessage(Message_v0_3 request,
+ List> consumers,
+ Consumer streamingErrorHandler) throws A2AClientException_v0_3 {
+ sendMessage(request, consumers, streamingErrorHandler, null);
+ }
+
+ /**
+ * Send a message to the remote agent. This method will automatically use
+ * the streaming or non-streaming approach as determined by the server's
+ * agent card and the client configuration. The specified client consumers
+ * will be used to handle messages, tasks, and update events received
+ * from the remote agent. The specified streaming error handler will be used
+ * if an error occurs during streaming. The configured client push notification
+ * configuration will get used for streaming.
+ *
+ * @param request the message
+ * @param consumers a list of consumers to pass responses from the remote agent to
+ * @param streamingErrorHandler an error handler that should be used for the streaming case if an error occurs
+ * @param context optional client call context for the request (may be {@code null})
+ * @throws A2AClientException_v0_3 if sending the message fails for any reason
+ */
+ public abstract void sendMessage(Message_v0_3 request,
+ List> consumers,
+ Consumer streamingErrorHandler,
+ @Nullable ClientCallContext_v0_3 context) throws A2AClientException_v0_3;
+
+ /**
+ * Send a message to the remote agent. This method will automatically use
+ * the streaming or non-streaming approach as determined by the server's
+ * agent card and the client configuration. The configured client consumers
+ * will be used to handle messages, tasks, and update events received from
+ * the remote agent. The configured streaming error handler will be used
+ * if an error occurs during streaming.
+ *
+ * @param request the message
+ * @param pushNotificationConfiguration the push notification configuration that should be
+ * used if the streaming approach is used
+ * @param metadata the optional metadata to include when sending the message
+ * @throws A2AClientException_v0_3 if sending the message fails for any reason
+ */
+ public void sendMessage(Message_v0_3 request, PushNotificationConfig_v0_3 pushNotificationConfiguration,
+ Map metadata) throws A2AClientException_v0_3 {
+ sendMessage(request, pushNotificationConfiguration, metadata, null);
+ }
+
+ /**
+ * Send a message to the remote agent. This method will automatically use
+ * the streaming or non-streaming approach as determined by the server's
+ * agent card and the client configuration. The configured client consumers
+ * will be used to handle messages, tasks, and update events received from
+ * the remote agent. The configured streaming error handler will be used
+ * if an error occurs during streaming.
+ *
+ * @param request the message
+ * @param pushNotificationConfiguration the push notification configuration that should be
+ * used if the streaming approach is used
+ * @param metadata the optional metadata to include when sending the message
+ * @param context optional client call context for the request (may be {@code null})
+ * @throws A2AClientException_v0_3 if sending the message fails for any reason
+ */
+ public abstract void sendMessage(Message_v0_3 request, PushNotificationConfig_v0_3 pushNotificationConfiguration,
+ Map metadata, @Nullable ClientCallContext_v0_3 context) throws A2AClientException_v0_3;
+
+ /**
+ * Retrieve the current state and history of a specific task.
+ *
+ * @param request the task query parameters specifying which task to retrieve
+ * @return the task
+ * @throws A2AClientException_v0_3 if retrieving the task fails for any reason
+ */
+ public Task_v0_3 getTask(TaskQueryParams_v0_3 request) throws A2AClientException_v0_3 {
+ return getTask(request, null);
+ }
+
+ /**
+ * Retrieve the current state and history of a specific task.
+ *
+ * @param request the task query parameters specifying which task to retrieve
+ * @param context optional client call context for the request (may be {@code null})
+ * @return the task
+ * @throws A2AClientException_v0_3 if retrieving the task fails for any reason
+ */
+ public abstract Task_v0_3 getTask(TaskQueryParams_v0_3 request, @Nullable ClientCallContext_v0_3 context) throws A2AClientException_v0_3;
+
+ /**
+ * Request the agent to cancel a specific task.
+ *
+ * @param request the task ID parameters specifying which task to cancel
+ * @return the cancelled task
+ * @throws A2AClientException_v0_3 if cancelling the task fails for any reason
+ */
+ public Task_v0_3 cancelTask(TaskIdParams_v0_3 request) throws A2AClientException_v0_3 {
+ return cancelTask(request, null);
+ }
+
+ /**
+ * Request the agent to cancel a specific task.
+ *
+ * @param request the task ID parameters specifying which task to cancel
+ * @param context optional client call context for the request (may be {@code null})
+ * @return the cancelled task
+ * @throws A2AClientException_v0_3 if cancelling the task fails for any reason
+ */
+ public abstract Task_v0_3 cancelTask(TaskIdParams_v0_3 request, @Nullable ClientCallContext_v0_3 context) throws A2AClientException_v0_3;
+
+ /**
+ * Set or update the push notification configuration for a specific task.
+ *
+ * @param request the push notification configuration to set for the task
+ * @return the configured TaskPushNotificationConfig
+ * @throws A2AClientException_v0_3 if setting the task push notification configuration fails for any reason
+ */
+ public TaskPushNotificationConfig_v0_3 setTaskPushNotificationConfiguration(
+ TaskPushNotificationConfig_v0_3 request) throws A2AClientException_v0_3 {
+ return setTaskPushNotificationConfiguration(request, null);
+ }
+
+ /**
+ * Set or update the push notification configuration for a specific task.
+ *
+ * @param request the push notification configuration to set for the task
+ * @param context optional client call context for the request (may be {@code null})
+ * @return the configured TaskPushNotificationConfig
+ * @throws A2AClientException_v0_3 if setting the task push notification configuration fails for any reason
+ */
+ public abstract TaskPushNotificationConfig_v0_3 setTaskPushNotificationConfiguration(
+ TaskPushNotificationConfig_v0_3 request,
+ @Nullable ClientCallContext_v0_3 context) throws A2AClientException_v0_3;
+
+ /**
+ * Retrieve the push notification configuration for a specific task.
+ *
+ * @param request the parameters specifying which task's notification config to retrieve
+ * @return the task push notification config
+ * @throws A2AClientException_v0_3 if getting the task push notification config fails for any reason
+ */
+ public TaskPushNotificationConfig_v0_3 getTaskPushNotificationConfiguration(
+ GetTaskPushNotificationConfigParams_v0_3 request) throws A2AClientException_v0_3 {
+ return getTaskPushNotificationConfiguration(request, null);
+ }
+
+ /**
+ * Retrieve the push notification configuration for a specific task.
+ *
+ * @param request the parameters specifying which task's notification config to retrieve
+ * @param context optional client call context for the request (may be {@code null})
+ * @return the task push notification config
+ * @throws A2AClientException_v0_3 if getting the task push notification config fails for any reason
+ */
+ public abstract TaskPushNotificationConfig_v0_3 getTaskPushNotificationConfiguration(
+ GetTaskPushNotificationConfigParams_v0_3 request,
+ @Nullable ClientCallContext_v0_3 context) throws A2AClientException_v0_3;
+
+ /**
+ * Retrieve the list of push notification configurations for a specific task.
+ *
+ * @param request the parameters specifying which task's notification configs to retrieve
+ * @return the list of task push notification configs
+ * @throws A2AClientException_v0_3 if getting the task push notification configs fails for any reason
+ */
+ public List listTaskPushNotificationConfigurations(
+ ListTaskPushNotificationConfigParams_v0_3 request) throws A2AClientException_v0_3 {
+ return listTaskPushNotificationConfigurations(request, null);
+ }
+
+ /**
+ * Retrieve the list of push notification configurations for a specific task.
+ *
+ * @param request the parameters specifying which task's notification configs to retrieve
+ * @param context optional client call context for the request (may be {@code null})
+ * @return the list of task push notification configs
+ * @throws A2AClientException_v0_3 if getting the task push notification configs fails for any reason
+ */
+ public abstract List listTaskPushNotificationConfigurations(
+ ListTaskPushNotificationConfigParams_v0_3 request,
+ @Nullable ClientCallContext_v0_3 context) throws A2AClientException_v0_3;
+
+ /**
+ * Delete the list of push notification configurations for a specific task.
+ *
+ * @param request the parameters specifying which task's notification configs to delete
+ * @throws A2AClientException_v0_3 if deleting the task push notification configs fails for any reason
+ */
+ public void deleteTaskPushNotificationConfigurations(
+ DeleteTaskPushNotificationConfigParams_v0_3 request) throws A2AClientException_v0_3 {
+ deleteTaskPushNotificationConfigurations(request, null);
+ }
+
+ /**
+ * Delete the list of push notification configurations for a specific task.
+ *
+ * @param request the parameters specifying which task's notification configs to delete
+ * @param context optional client call context for the request (may be {@code null})
+ * @throws A2AClientException_v0_3 if deleting the task push notification configs fails for any reason
+ */
+ public abstract void deleteTaskPushNotificationConfigurations(
+ DeleteTaskPushNotificationConfigParams_v0_3 request,
+ @Nullable ClientCallContext_v0_3 context) throws A2AClientException_v0_3;
+
+ /**
+ * Resubscribe to a task's event stream.
+ * This is only available if both the client and server support streaming.
+ * The configured client consumers will be used to handle messages, tasks,
+ * and update events received from the remote agent. The configured streaming
+ * error handler will be used if an error occurs during streaming.
+ *
+ * @param request the parameters specifying which task's notification configs to delete
+ * @throws A2AClientException_v0_3 if resubscribing fails for any reason
+ */
+ public void resubscribe(TaskIdParams_v0_3 request) throws A2AClientException_v0_3 {
+ resubscribe(request, null);
+ }
+
+ /**
+ * Resubscribe to a task's event stream.
+ * This is only available if both the client and server support streaming.
+ * The configured client consumers will be used to handle messages, tasks,
+ * and update events received from the remote agent. The configured streaming
+ * error handler will be used if an error occurs during streaming.
+ *
+ * @param request the parameters specifying which task's notification configs to delete
+ * @param context optional client call context for the request (may be {@code null})
+ * @throws A2AClientException_v0_3 if resubscribing fails for any reason
+ */
+ public abstract void resubscribe(TaskIdParams_v0_3 request, @Nullable ClientCallContext_v0_3 context) throws A2AClientException_v0_3;
+
+ /**
+ * Resubscribe to a task's event stream.
+ * This is only available if both the client and server support streaming.
+ * The specified client consumers will be used to handle messages, tasks, and
+ * update events received from the remote agent. The specified streaming error
+ * handler will be used if an error occurs during streaming.
+ *
+ * @param request the parameters specifying which task's notification configs to delete
+ * @param consumers a list of consumers to pass responses from the remote agent to
+ * @param streamingErrorHandler an error handler that should be used for the streaming case if an error occurs
+ * @throws A2AClientException_v0_3 if resubscribing fails for any reason
+ */
+ public void resubscribe(TaskIdParams_v0_3 request, List> consumers,
+ Consumer streamingErrorHandler) throws A2AClientException_v0_3 {
+ resubscribe(request, consumers, streamingErrorHandler, null);
+ }
+
+ /**
+ * Resubscribe to a task's event stream.
+ * This is only available if both the client and server support streaming.
+ * The specified client consumers will be used to handle messages, tasks, and
+ * update events received from the remote agent. The specified streaming error
+ * handler will be used if an error occurs during streaming.
+ *
+ * @param request the parameters specifying which task's notification configs to delete
+ * @param consumers a list of consumers to pass responses from the remote agent to
+ * @param streamingErrorHandler an error handler that should be used for the streaming case if an error occurs
+ * @param context optional client call context for the request (may be {@code null})
+ * @throws A2AClientException_v0_3 if resubscribing fails for any reason
+ */
+ public abstract void resubscribe(TaskIdParams_v0_3 request, List> consumers,
+ Consumer streamingErrorHandler, @Nullable ClientCallContext_v0_3 context) throws A2AClientException_v0_3;
+
+ /**
+ * Retrieve the AgentCard.
+ *
+ * @return the AgentCard
+ * @throws A2AClientException_v0_3 if retrieving the agent card fails for any reason
+ */
+ public AgentCard_v0_3 getAgentCard() throws A2AClientException_v0_3 {
+ return getAgentCard(null);
+ }
+
+ /**
+ * Retrieve the AgentCard.
+ *
+ * @param context optional client call context for the request (may be {@code null})
+ * @return the AgentCard
+ * @throws A2AClientException_v0_3 if retrieving the agent card fails for any reason
+ */
+ public abstract AgentCard_v0_3 getAgentCard(@Nullable ClientCallContext_v0_3 context) throws A2AClientException_v0_3;
+
+ /**
+ * Close the transport and release any associated resources.
+ */
+ public abstract void close();
+
+ /**
+ * Process the event using all configured consumers.
+ */
+ void consume(ClientEvent_v0_3 clientEventOrMessage, AgentCard_v0_3 agentCard) {
+ for (BiConsumer consumer : consumers) {
+ consumer.accept(clientEventOrMessage, agentCard);
+ }
+ }
+
+ /**
+ * Get the error handler that should be used during streaming.
+ *
+ * @return the streaming error handler
+ */
+ public @Nullable Consumer getStreamingErrorHandler() {
+ return streamingErrorHandler;
+ }
+
+}
\ No newline at end of file
diff --git a/compat-0.3/client/base/src/main/java/org/a2aproject/sdk/compat03/client/ClientBuilder_v0_3.java b/compat-0.3/client/base/src/main/java/org/a2aproject/sdk/compat03/client/ClientBuilder_v0_3.java
new file mode 100644
index 000000000..b2fe661c2
--- /dev/null
+++ b/compat-0.3/client/base/src/main/java/org/a2aproject/sdk/compat03/client/ClientBuilder_v0_3.java
@@ -0,0 +1,169 @@
+package org.a2aproject.sdk.compat03.client;
+
+import org.a2aproject.sdk.compat03.client.config.ClientConfig_v0_3;
+import org.a2aproject.sdk.compat03.client.transport.spi.ClientTransport_v0_3;
+import org.a2aproject.sdk.compat03.client.transport.spi.ClientTransportConfig_v0_3;
+import org.a2aproject.sdk.compat03.client.transport.spi.ClientTransportConfigBuilder_v0_3;
+import org.a2aproject.sdk.compat03.client.transport.spi.ClientTransportProvider_v0_3;
+import org.a2aproject.sdk.compat03.spec.A2AClientException_v0_3;
+import org.a2aproject.sdk.compat03.spec.AgentCard_v0_3;
+import org.a2aproject.sdk.compat03.spec.AgentInterface_v0_3;
+import org.a2aproject.sdk.compat03.spec.TransportProtocol_v0_3;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.ServiceLoader;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
+public class ClientBuilder_v0_3 {
+
+ private static final Map>> transportProviderRegistry = new HashMap<>();
+ private static final Map, String> transportProtocolMapping = new HashMap<>();
+
+ static {
+ ServiceLoader loader = ServiceLoader.load(ClientTransportProvider_v0_3.class);
+ for (ClientTransportProvider_v0_3, ?> transport : loader) {
+ transportProviderRegistry.put(transport.getTransportProtocol(), transport);
+ transportProtocolMapping.put(transport.getTransportProtocolClass(), transport.getTransportProtocol());
+ }
+ }
+
+ private final AgentCard_v0_3 agentCard;
+
+ private final List> consumers = new ArrayList<>();
+ private @Nullable Consumer streamErrorHandler;
+ private ClientConfig_v0_3 clientConfig = new ClientConfig_v0_3.Builder().build();
+
+ private final Map, ClientTransportConfig_v0_3 extends ClientTransport_v0_3>> clientTransports = new LinkedHashMap<>();
+
+ ClientBuilder_v0_3(@NonNull AgentCard_v0_3 agentCard) {
+ this.agentCard = agentCard;
+ }
+
+ public ClientBuilder_v0_3 withTransport(Class clazz, ClientTransportConfigBuilder_v0_3 extends ClientTransportConfig_v0_3, ?> configBuilder) {
+ return withTransport(clazz, configBuilder.build());
+ }
+
+ public ClientBuilder_v0_3 withTransport(Class clazz, ClientTransportConfig_v0_3 config) {
+ clientTransports.put(clazz, config);
+
+ return this;
+ }
+
+ public ClientBuilder_v0_3 addConsumer(BiConsumer consumer) {
+ this.consumers.add(consumer);
+ return this;
+ }
+
+ public ClientBuilder_v0_3 addConsumers(List> consumers) {
+ this.consumers.addAll(consumers);
+ return this;
+ }
+
+ public ClientBuilder_v0_3 streamingErrorHandler(Consumer streamErrorHandler) {
+ this.streamErrorHandler = streamErrorHandler;
+ return this;
+ }
+
+ public ClientBuilder_v0_3 clientConfig(@NonNull ClientConfig_v0_3 clientConfig) {
+ this.clientConfig = clientConfig;
+ return this;
+ }
+
+ public Client_v0_3 build() throws A2AClientException_v0_3 {
+ if (this.clientConfig == null) {
+ this.clientConfig = new ClientConfig_v0_3.Builder().build();
+ }
+
+ ClientTransport_v0_3 clientTransport = buildClientTransport();
+
+ return new Client_v0_3(agentCard, clientConfig, clientTransport, consumers, streamErrorHandler);
+ }
+
+ @SuppressWarnings("unchecked")
+ private ClientTransport_v0_3 buildClientTransport() throws A2AClientException_v0_3 {
+ // Get the preferred transport
+ AgentInterface_v0_3 agentInterface = findBestClientTransport();
+
+ // Get the transport provider associated with the protocol
+ ClientTransportProvider_v0_3 clientTransportProvider = transportProviderRegistry.get(agentInterface.transport());
+ if (clientTransportProvider == null) {
+ throw new A2AClientException_v0_3("No client available for " + agentInterface.transport());
+ }
+ Class extends ClientTransport_v0_3> transportProtocolClass = clientTransportProvider.getTransportProtocolClass();
+
+ // Retrieve the configuration associated with the preferred transport
+ ClientTransportConfig_v0_3 extends ClientTransport_v0_3> clientTransportConfig = clientTransports.get(transportProtocolClass);
+
+ if (clientTransportConfig == null) {
+ throw new A2AClientException_v0_3("Missing required TransportConfig for " + agentInterface.transport());
+ }
+
+ return clientTransportProvider.create(clientTransportConfig, agentCard, agentInterface.url());
+ }
+
+ private Map getServerPreferredTransports() {
+ Map serverPreferredTransports = new LinkedHashMap<>();
+ serverPreferredTransports.put(agentCard.preferredTransport(), agentCard.url());
+ if (agentCard.additionalInterfaces() != null) {
+ for (AgentInterface_v0_3 agentInterface : agentCard.additionalInterfaces()) {
+ serverPreferredTransports.putIfAbsent(agentInterface.transport(), agentInterface.url());
+ }
+ }
+ return serverPreferredTransports;
+ }
+
+ private List getClientPreferredTransports() {
+ List supportedClientTransports = new ArrayList<>();
+
+ if (clientTransports.isEmpty()) {
+ // default to JSONRPC if not specified
+ supportedClientTransports.add(TransportProtocol_v0_3.JSONRPC.asString());
+ } else {
+ clientTransports.forEach((aClass, clientTransportConfig) -> supportedClientTransports.add(transportProtocolMapping.get(aClass)));
+ }
+ return supportedClientTransports;
+ }
+
+ private AgentInterface_v0_3 findBestClientTransport() throws A2AClientException_v0_3 {
+ // Retrieve transport supported by the A2A server
+ Map serverPreferredTransports = getServerPreferredTransports();
+
+ // Retrieve transport configured for this client (using withTransport methods)
+ List clientPreferredTransports = getClientPreferredTransports();
+
+ String transportProtocol = null;
+ String transportUrl = null;
+ if (clientConfig.isUseClientPreference()) {
+ for (String clientPreferredTransport : clientPreferredTransports) {
+ if (serverPreferredTransports.containsKey(clientPreferredTransport)) {
+ transportProtocol = clientPreferredTransport;
+ transportUrl = serverPreferredTransports.get(transportProtocol);
+ break;
+ }
+ }
+ } else {
+ for (Map.Entry transport : serverPreferredTransports.entrySet()) {
+ if (clientPreferredTransports.contains(transport.getKey())) {
+ transportProtocol = transport.getKey();
+ transportUrl = transport.getValue();
+ break;
+ }
+ }
+ }
+ if (transportProtocol == null || transportUrl == null) {
+ throw new A2AClientException_v0_3("No compatible transport found");
+ }
+ if (! transportProviderRegistry.containsKey(transportProtocol)) {
+ throw new A2AClientException_v0_3("No client available for " + transportProtocol);
+ }
+
+ return new AgentInterface_v0_3(transportProtocol, transportUrl);
+ }
+}
diff --git a/compat-0.3/client/base/src/main/java/org/a2aproject/sdk/compat03/client/ClientEvent_v0_3.java b/compat-0.3/client/base/src/main/java/org/a2aproject/sdk/compat03/client/ClientEvent_v0_3.java
new file mode 100644
index 000000000..498814a56
--- /dev/null
+++ b/compat-0.3/client/base/src/main/java/org/a2aproject/sdk/compat03/client/ClientEvent_v0_3.java
@@ -0,0 +1,4 @@
+package org.a2aproject.sdk.compat03.client;
+
+public sealed interface ClientEvent_v0_3 permits MessageEvent_v0_3, TaskEvent_v0_3, TaskUpdateEvent_v0_3 {
+}
diff --git a/compat-0.3/client/base/src/main/java/org/a2aproject/sdk/compat03/client/ClientTaskManager_v0_3.java b/compat-0.3/client/base/src/main/java/org/a2aproject/sdk/compat03/client/ClientTaskManager_v0_3.java
new file mode 100644
index 000000000..0a29a0aa6
--- /dev/null
+++ b/compat-0.3/client/base/src/main/java/org/a2aproject/sdk/compat03/client/ClientTaskManager_v0_3.java
@@ -0,0 +1,139 @@
+package org.a2aproject.sdk.compat03.client;
+
+import static org.a2aproject.sdk.compat03.util.Utils_v0_3.appendArtifactToTask;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.a2aproject.sdk.compat03.spec.A2AClientError_v0_3;
+import org.a2aproject.sdk.compat03.spec.A2AClientInvalidArgsError_v0_3;
+import org.a2aproject.sdk.compat03.spec.A2AClientInvalidStateError_v0_3;
+import org.a2aproject.sdk.compat03.spec.Message_v0_3;
+import org.a2aproject.sdk.compat03.spec.Task_v0_3;
+import org.a2aproject.sdk.compat03.spec.TaskArtifactUpdateEvent_v0_3;
+import org.a2aproject.sdk.compat03.spec.TaskState_v0_3;
+import org.a2aproject.sdk.compat03.spec.TaskStatus_v0_3;
+import org.a2aproject.sdk.compat03.spec.TaskStatusUpdateEvent_v0_3;
+import org.jspecify.annotations.Nullable;
+
+/**
+ * Helps manage a task's lifecycle during the execution of a request.
+ * Responsible for retrieving, saving, and updating the task based on
+ * events received from the agent.
+ */
+public class ClientTaskManager_v0_3 {
+
+ private @Nullable Task_v0_3 currentTask;
+ private @Nullable String taskId;
+ private @Nullable String contextId;
+
+ public ClientTaskManager_v0_3() {
+ this.currentTask = null;
+ this.taskId = null;
+ this.contextId = null;
+ }
+
+ public Task_v0_3 getCurrentTask() throws A2AClientInvalidStateError_v0_3 {
+ if (currentTask == null) {
+ throw new A2AClientInvalidStateError_v0_3("No current task");
+ }
+ return currentTask;
+ }
+
+ public Task_v0_3 saveTaskEvent(Task_v0_3 task) throws A2AClientInvalidArgsError_v0_3 {
+ if (currentTask != null) {
+ throw new A2AClientInvalidArgsError_v0_3("Task is already set, create new manager for new tasks.");
+ }
+ saveTask(task);
+ return task;
+ }
+
+ public Task_v0_3 saveTaskEvent(TaskStatusUpdateEvent_v0_3 taskStatusUpdateEvent) throws A2AClientError_v0_3 {
+ if (taskId == null) {
+ taskId = taskStatusUpdateEvent.getTaskId();
+ }
+ if (contextId == null) {
+ contextId = taskStatusUpdateEvent.getContextId();
+ }
+ Task_v0_3 task = currentTask;
+ if (task == null) {
+ task = new Task_v0_3.Builder()
+ .status(new TaskStatus_v0_3(TaskState_v0_3.UNKNOWN))
+ .id(taskId)
+ .contextId(contextId == null ? "" : contextId)
+ .build();
+ }
+
+ Task_v0_3.Builder taskBuilder = new Task_v0_3.Builder(task);
+ if (taskStatusUpdateEvent.getStatus().message() != null) {
+ if (task.getHistory() == null) {
+ taskBuilder.history(taskStatusUpdateEvent.getStatus().message());
+ } else {
+ List history = new ArrayList<>(task.getHistory());
+ history.add(taskStatusUpdateEvent.getStatus().message());
+ taskBuilder.history(history);
+ }
+ }
+ if (taskStatusUpdateEvent.getMetadata() != null) {
+ Map newMetadata = task.getMetadata() != null ? new HashMap<>(task.getMetadata()) : new HashMap<>();
+ newMetadata.putAll(taskStatusUpdateEvent.getMetadata());
+ taskBuilder.metadata(newMetadata);
+ }
+ taskBuilder.status(taskStatusUpdateEvent.getStatus());
+ currentTask = taskBuilder.build();
+ return currentTask;
+ }
+
+ public Task_v0_3 saveTaskEvent(TaskArtifactUpdateEvent_v0_3 taskArtifactUpdateEvent) {
+ if (taskId == null) {
+ taskId = taskArtifactUpdateEvent.getTaskId();
+ }
+ if (contextId == null) {
+ contextId = taskArtifactUpdateEvent.getContextId();
+ }
+ Task_v0_3 task = currentTask;
+ if (task == null) {
+ task = new Task_v0_3.Builder()
+ .status(new TaskStatus_v0_3(TaskState_v0_3.UNKNOWN))
+ .id(taskId)
+ .contextId(contextId == null ? "" : contextId)
+ .build();
+ }
+ currentTask = appendArtifactToTask(task, taskArtifactUpdateEvent, taskId);
+ return currentTask;
+ }
+
+ /**
+ * Update a task by adding a message to its history. If the task has a message in its current status,
+ * that message is moved to the history first.
+ *
+ * @param message the new message to add to the history
+ * @param task the task to update
+ * @return the updated task
+ */
+ public Task_v0_3 updateWithMessage(Message_v0_3 message, Task_v0_3 task) {
+ Task_v0_3.Builder taskBuilder = new Task_v0_3.Builder(task);
+ List history = task.getHistory();
+ if (history == null) {
+ history = new ArrayList<>();
+ }
+ if (task.getStatus().message() != null) {
+ history.add(task.getStatus().message());
+ taskBuilder.status(new TaskStatus_v0_3(task.getStatus().state(), null, task.getStatus().timestamp()));
+ }
+ history.add(message);
+ taskBuilder.history(history);
+ currentTask = taskBuilder.build();
+ return currentTask;
+ }
+
+ private void saveTask(Task_v0_3 task) {
+ currentTask = task;
+ if (taskId == null) {
+ taskId = currentTask.getId();
+ contextId = currentTask.getContextId();
+ }
+ }
+}
\ No newline at end of file
diff --git a/compat-0.3/client/base/src/main/java/org/a2aproject/sdk/compat03/client/Client_v0_3.java b/compat-0.3/client/base/src/main/java/org/a2aproject/sdk/compat03/client/Client_v0_3.java
new file mode 100644
index 000000000..d264326d2
--- /dev/null
+++ b/compat-0.3/client/base/src/main/java/org/a2aproject/sdk/compat03/client/Client_v0_3.java
@@ -0,0 +1,243 @@
+package org.a2aproject.sdk.compat03.client;
+
+import java.util.List;
+import java.util.Map;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+
+import org.a2aproject.sdk.compat03.client.config.ClientConfig_v0_3;
+import org.a2aproject.sdk.compat03.client.transport.spi.interceptors.ClientCallContext_v0_3;
+import org.a2aproject.sdk.compat03.client.transport.spi.ClientTransport_v0_3;
+import org.a2aproject.sdk.compat03.spec.A2AClientError_v0_3;
+import org.a2aproject.sdk.compat03.spec.A2AClientException_v0_3;
+import org.a2aproject.sdk.compat03.spec.A2AClientInvalidStateError_v0_3;
+import org.a2aproject.sdk.compat03.spec.AgentCard_v0_3;
+import org.a2aproject.sdk.compat03.spec.DeleteTaskPushNotificationConfigParams_v0_3;
+import org.a2aproject.sdk.compat03.spec.EventKind_v0_3;
+import org.a2aproject.sdk.compat03.spec.GetTaskPushNotificationConfigParams_v0_3;
+import org.a2aproject.sdk.compat03.spec.ListTaskPushNotificationConfigParams_v0_3;
+import org.a2aproject.sdk.compat03.spec.Message_v0_3;
+import org.a2aproject.sdk.compat03.spec.MessageSendConfiguration_v0_3;
+import org.a2aproject.sdk.compat03.spec.MessageSendParams_v0_3;
+import org.a2aproject.sdk.compat03.spec.PushNotificationConfig_v0_3;
+import org.a2aproject.sdk.compat03.spec.StreamingEventKind_v0_3;
+import org.a2aproject.sdk.compat03.spec.Task_v0_3;
+import org.a2aproject.sdk.compat03.spec.TaskArtifactUpdateEvent_v0_3;
+import org.a2aproject.sdk.compat03.spec.TaskIdParams_v0_3;
+import org.a2aproject.sdk.compat03.spec.TaskPushNotificationConfig_v0_3;
+import org.a2aproject.sdk.compat03.spec.TaskQueryParams_v0_3;
+import org.a2aproject.sdk.compat03.spec.TaskStatusUpdateEvent_v0_3;
+
+import static org.a2aproject.sdk.util.Assert.checkNotNullParam;
+
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
+public class Client_v0_3 extends AbstractClient_v0_3 {
+
+ private final ClientConfig_v0_3 clientConfig;
+ private final ClientTransport_v0_3 clientTransport;
+ private AgentCard_v0_3 agentCard;
+
+ Client_v0_3(AgentCard_v0_3 agentCard, ClientConfig_v0_3 clientConfig, ClientTransport_v0_3 clientTransport,
+ List> consumers, @Nullable Consumer streamingErrorHandler) {
+ super(consumers, streamingErrorHandler);
+ checkNotNullParam("agentCard", agentCard);
+
+ this.agentCard = agentCard;
+ this.clientConfig = clientConfig;
+ this.clientTransport = clientTransport;
+ }
+
+ public static ClientBuilder_v0_3 builder(AgentCard_v0_3 agentCard) {
+ return new ClientBuilder_v0_3(agentCard);
+ }
+
+ @Override
+ public void sendMessage(Message_v0_3 request, @Nullable ClientCallContext_v0_3 context) throws A2AClientException_v0_3 {
+ MessageSendParams_v0_3 messageSendParams = getMessageSendParams(request, clientConfig);
+ sendMessage(messageSendParams, null, null, context);
+ }
+
+ @Override
+ public void sendMessage(Message_v0_3 request, List> consumers,
+ Consumer streamingErrorHandler, @Nullable ClientCallContext_v0_3 context) throws A2AClientException_v0_3 {
+ MessageSendParams_v0_3 messageSendParams = getMessageSendParams(request, clientConfig);
+ sendMessage(messageSendParams, consumers, streamingErrorHandler, context);
+ }
+
+ @Override
+ public void sendMessage(Message_v0_3 request, PushNotificationConfig_v0_3 pushNotificationConfiguration,
+ Map metatadata, @Nullable ClientCallContext_v0_3 context) throws A2AClientException_v0_3 {
+ MessageSendConfiguration_v0_3 messageSendConfiguration = createMessageSendConfiguration(pushNotificationConfiguration);
+
+ MessageSendParams_v0_3 messageSendParams = new MessageSendParams_v0_3.Builder()
+ .message(request)
+ .configuration(messageSendConfiguration)
+ .metadata(metatadata)
+ .build();
+
+ sendMessage(messageSendParams, null, null, context);
+ }
+
+ @Override
+ public Task_v0_3 getTask(TaskQueryParams_v0_3 request, @Nullable ClientCallContext_v0_3 context) throws A2AClientException_v0_3 {
+ return clientTransport.getTask(request, context);
+ }
+
+ @Override
+ public Task_v0_3 cancelTask(TaskIdParams_v0_3 request, @Nullable ClientCallContext_v0_3 context) throws A2AClientException_v0_3 {
+ return clientTransport.cancelTask(request, context);
+ }
+
+ @Override
+ public TaskPushNotificationConfig_v0_3 setTaskPushNotificationConfiguration(
+ TaskPushNotificationConfig_v0_3 request, @Nullable ClientCallContext_v0_3 context) throws A2AClientException_v0_3 {
+ return clientTransport.setTaskPushNotificationConfiguration(request, context);
+ }
+
+ @Override
+ public TaskPushNotificationConfig_v0_3 getTaskPushNotificationConfiguration(
+ GetTaskPushNotificationConfigParams_v0_3 request, @Nullable ClientCallContext_v0_3 context) throws A2AClientException_v0_3 {
+ return clientTransport.getTaskPushNotificationConfiguration(request, context);
+ }
+
+ @Override
+ public List listTaskPushNotificationConfigurations(
+ ListTaskPushNotificationConfigParams_v0_3 request, @Nullable ClientCallContext_v0_3 context) throws A2AClientException_v0_3 {
+ return clientTransport.listTaskPushNotificationConfigurations(request, context);
+ }
+
+ @Override
+ public void deleteTaskPushNotificationConfigurations(
+ DeleteTaskPushNotificationConfigParams_v0_3 request, @Nullable ClientCallContext_v0_3 context) throws A2AClientException_v0_3 {
+ clientTransport.deleteTaskPushNotificationConfigurations(request, context);
+ }
+
+ @Override
+ public void resubscribe(TaskIdParams_v0_3 request, @Nullable ClientCallContext_v0_3 context) throws A2AClientException_v0_3 {
+ resubscribeToTask(request, null, null, context);
+ }
+
+ @Override
+ public void resubscribe(TaskIdParams_v0_3 request, @Nullable List> consumers,
+ @Nullable Consumer streamingErrorHandler, @Nullable ClientCallContext_v0_3 context) throws A2AClientException_v0_3 {
+ resubscribeToTask(request, consumers, streamingErrorHandler, context);
+ }
+
+ @Override
+ public AgentCard_v0_3 getAgentCard(@Nullable ClientCallContext_v0_3 context) throws A2AClientException_v0_3 {
+ agentCard = clientTransport.getAgentCard(context);
+ return agentCard;
+ }
+
+ @Override
+ public void close() {
+ clientTransport.close();
+ }
+
+ private ClientEvent_v0_3 getClientEvent(StreamingEventKind_v0_3 event, ClientTaskManager_v0_3 taskManager) throws A2AClientError_v0_3 {
+ if (event instanceof Message_v0_3 message) {
+ return new MessageEvent_v0_3(message);
+ } else if (event instanceof Task_v0_3 task) {
+ taskManager.saveTaskEvent(task);
+ return new TaskEvent_v0_3(taskManager.getCurrentTask());
+ } else if (event instanceof TaskStatusUpdateEvent_v0_3 updateEvent) {
+ taskManager.saveTaskEvent(updateEvent);
+ return new TaskUpdateEvent_v0_3(taskManager.getCurrentTask(), updateEvent);
+ } else if (event instanceof TaskArtifactUpdateEvent_v0_3 updateEvent) {
+ taskManager.saveTaskEvent(updateEvent);
+ return new TaskUpdateEvent_v0_3(taskManager.getCurrentTask(), updateEvent);
+ } else {
+ throw new A2AClientInvalidStateError_v0_3("Invalid client event");
+ }
+ }
+
+ private MessageSendConfiguration_v0_3 createMessageSendConfiguration(@Nullable PushNotificationConfig_v0_3 pushNotificationConfig) {
+ return new MessageSendConfiguration_v0_3.Builder()
+ .acceptedOutputModes(clientConfig.getAcceptedOutputModes())
+ .blocking(!clientConfig.isPolling())
+ .historyLength(clientConfig.getHistoryLength())
+ .pushNotificationConfig(pushNotificationConfig)
+ .build();
+ }
+
+ private void sendMessage(MessageSendParams_v0_3 messageSendParams, @Nullable List> consumers,
+ @Nullable Consumer errorHandler, @Nullable ClientCallContext_v0_3 context) throws A2AClientException_v0_3 {
+ if (! clientConfig.isStreaming() || ! agentCard.capabilities().streaming()) {
+ EventKind_v0_3 eventKind = clientTransport.sendMessage(messageSendParams, context);
+ ClientEvent_v0_3 clientEvent;
+ if (eventKind instanceof Task_v0_3 task) {
+ clientEvent = new TaskEvent_v0_3(task);
+ } else {
+ // must be a message
+ clientEvent = new MessageEvent_v0_3((Message_v0_3) eventKind);
+ }
+ consume(clientEvent, agentCard, consumers);
+ } else {
+ ClientTaskManager_v0_3 tracker = new ClientTaskManager_v0_3();
+ Consumer overriddenErrorHandler = getOverriddenErrorHandler(errorHandler);
+ Consumer eventHandler = event -> {
+ try {
+ ClientEvent_v0_3 clientEvent = getClientEvent(event, tracker);
+ consume(clientEvent, agentCard, consumers);
+ } catch (A2AClientError_v0_3 e) {
+ overriddenErrorHandler.accept(e);
+ }
+ };
+ clientTransport.sendMessageStreaming(messageSendParams, eventHandler, overriddenErrorHandler, context);
+ }
+ }
+
+ private void resubscribeToTask(TaskIdParams_v0_3 request, @Nullable List> consumers,
+ @Nullable Consumer errorHandler, @Nullable ClientCallContext_v0_3 context) throws A2AClientException_v0_3 {
+ if (! clientConfig.isStreaming() || ! agentCard.capabilities().streaming()) {
+ throw new A2AClientException_v0_3("Client and/or server does not support resubscription");
+ }
+ ClientTaskManager_v0_3 tracker = new ClientTaskManager_v0_3();
+ Consumer overriddenErrorHandler = getOverriddenErrorHandler(errorHandler);
+ Consumer eventHandler = event -> {
+ try {
+ ClientEvent_v0_3 clientEvent = getClientEvent(event, tracker);
+ consume(clientEvent, agentCard, consumers);
+ } catch (A2AClientError_v0_3 e) {
+ overriddenErrorHandler.accept(e);
+ }
+ };
+ clientTransport.resubscribe(request, eventHandler, overriddenErrorHandler, context);
+ }
+
+ private @NonNull Consumer getOverriddenErrorHandler(@Nullable Consumer errorHandler) {
+ return e -> {
+ if (errorHandler != null) {
+ errorHandler.accept(e);
+ } else {
+ if (getStreamingErrorHandler() != null) {
+ getStreamingErrorHandler().accept(e);
+ }
+ }
+ };
+ }
+
+ private void consume(ClientEvent_v0_3 clientEvent, AgentCard_v0_3 agentCard, @Nullable List> consumers) {
+ if (consumers != null) {
+ // use specified consumers
+ for (BiConsumer consumer : consumers) {
+ consumer.accept(clientEvent, agentCard);
+ }
+ } else {
+ // use configured consumers
+ consume(clientEvent, agentCard);
+ }
+ }
+
+ private MessageSendParams_v0_3 getMessageSendParams(Message_v0_3 request, ClientConfig_v0_3 clientConfig) {
+ MessageSendConfiguration_v0_3 messageSendConfiguration = createMessageSendConfiguration(clientConfig.getPushNotificationConfig());
+
+ return new MessageSendParams_v0_3.Builder()
+ .message(request)
+ .configuration(messageSendConfiguration)
+ .metadata(clientConfig.getMetadata())
+ .build();
+ }
+}
diff --git a/compat-0.3/client/base/src/main/java/org/a2aproject/sdk/compat03/client/MessageEvent_v0_3.java b/compat-0.3/client/base/src/main/java/org/a2aproject/sdk/compat03/client/MessageEvent_v0_3.java
new file mode 100644
index 000000000..2c321e0da
--- /dev/null
+++ b/compat-0.3/client/base/src/main/java/org/a2aproject/sdk/compat03/client/MessageEvent_v0_3.java
@@ -0,0 +1,26 @@
+package org.a2aproject.sdk.compat03.client;
+
+import org.a2aproject.sdk.compat03.spec.Message_v0_3;
+
+/**
+ * A message event received by a client.
+ */
+public final class MessageEvent_v0_3 implements ClientEvent_v0_3 {
+
+ private final Message_v0_3 message;
+
+ /**
+ * A message event.
+ *
+ * @param message the message received
+ */
+ public MessageEvent_v0_3(Message_v0_3 message) {
+ this.message = message;
+ }
+
+ public Message_v0_3 getMessage() {
+ return message;
+ }
+}
+
+
diff --git a/compat-0.3/client/base/src/main/java/org/a2aproject/sdk/compat03/client/TaskEvent_v0_3.java b/compat-0.3/client/base/src/main/java/org/a2aproject/sdk/compat03/client/TaskEvent_v0_3.java
new file mode 100644
index 000000000..504fe8559
--- /dev/null
+++ b/compat-0.3/client/base/src/main/java/org/a2aproject/sdk/compat03/client/TaskEvent_v0_3.java
@@ -0,0 +1,27 @@
+package org.a2aproject.sdk.compat03.client;
+
+import static org.a2aproject.sdk.util.Assert.checkNotNullParam;
+
+import org.a2aproject.sdk.compat03.spec.Task_v0_3;
+
+/**
+ * A task event received by a client.
+ */
+public final class TaskEvent_v0_3 implements ClientEvent_v0_3 {
+
+ private final Task_v0_3 task;
+
+ /**
+ * A client task event.
+ *
+ * @param task the task received
+ */
+ public TaskEvent_v0_3(Task_v0_3 task) {
+ checkNotNullParam("task", task);
+ this.task = task;
+ }
+
+ public Task_v0_3 getTask() {
+ return task;
+ }
+}
diff --git a/compat-0.3/client/base/src/main/java/org/a2aproject/sdk/compat03/client/TaskUpdateEvent_v0_3.java b/compat-0.3/client/base/src/main/java/org/a2aproject/sdk/compat03/client/TaskUpdateEvent_v0_3.java
new file mode 100644
index 000000000..9459a2b04
--- /dev/null
+++ b/compat-0.3/client/base/src/main/java/org/a2aproject/sdk/compat03/client/TaskUpdateEvent_v0_3.java
@@ -0,0 +1,37 @@
+package org.a2aproject.sdk.compat03.client;
+
+import static org.a2aproject.sdk.util.Assert.checkNotNullParam;
+
+import org.a2aproject.sdk.compat03.spec.Task_v0_3;
+import org.a2aproject.sdk.compat03.spec.UpdateEvent_v0_3;
+
+/**
+ * A task update event received by a client.
+ */
+public final class TaskUpdateEvent_v0_3 implements ClientEvent_v0_3 {
+
+ private final Task_v0_3 task;
+ private final UpdateEvent_v0_3 updateEvent;
+
+ /**
+ * A task update event.
+ *
+ * @param task the current task
+ * @param updateEvent the update event received for the current task
+ */
+ public TaskUpdateEvent_v0_3(Task_v0_3 task, UpdateEvent_v0_3 updateEvent) {
+ checkNotNullParam("task", task);
+ checkNotNullParam("updateEvent", updateEvent);
+ this.task = task;
+ this.updateEvent = updateEvent;
+ }
+
+ public Task_v0_3 getTask() {
+ return task;
+ }
+
+ public UpdateEvent_v0_3 getUpdateEvent() {
+ return updateEvent;
+ }
+
+}
diff --git a/compat-0.3/client/base/src/main/java/org/a2aproject/sdk/compat03/client/config/ClientConfig_v0_3.java b/compat-0.3/client/base/src/main/java/org/a2aproject/sdk/compat03/client/config/ClientConfig_v0_3.java
new file mode 100644
index 000000000..ca143a45e
--- /dev/null
+++ b/compat-0.3/client/base/src/main/java/org/a2aproject/sdk/compat03/client/config/ClientConfig_v0_3.java
@@ -0,0 +1,114 @@
+package org.a2aproject.sdk.compat03.client.config;
+
+import java.util.List;
+import java.util.Map;
+
+import org.a2aproject.sdk.compat03.spec.PushNotificationConfig_v0_3;
+import java.util.ArrayList;
+import java.util.HashMap;
+import org.jspecify.annotations.Nullable;
+
+/**
+ * Configuration for the A2A client factory.
+ */
+public class ClientConfig_v0_3 {
+
+ private final Boolean streaming;
+ private final Boolean polling;
+ private final Boolean useClientPreference;
+ private final List acceptedOutputModes;
+ private final @Nullable PushNotificationConfig_v0_3 pushNotificationConfig;
+ private final @Nullable Integer historyLength;
+ private final Map metadata;
+
+ private ClientConfig_v0_3(Builder builder) {
+ this.streaming = builder.streaming == null ? true : builder.streaming;
+ this.polling = builder.polling == null ? false : builder.polling;
+ this.useClientPreference = builder.useClientPreference == null ? false : builder.useClientPreference;
+ this.acceptedOutputModes = builder.acceptedOutputModes;
+ this.pushNotificationConfig = builder.pushNotificationConfig;
+ this.historyLength = builder.historyLength;
+ this.metadata = builder.metadata;
+ }
+
+ public boolean isStreaming() {
+ return streaming;
+ }
+
+ public boolean isPolling() {
+ return polling;
+ }
+
+ public boolean isUseClientPreference() {
+ return useClientPreference;
+ }
+
+ public List getAcceptedOutputModes() {
+ return acceptedOutputModes;
+ }
+
+ public @Nullable PushNotificationConfig_v0_3 getPushNotificationConfig() {
+ return pushNotificationConfig;
+ }
+
+ public @Nullable Integer getHistoryLength() {
+ return historyLength;
+ }
+
+ public Map getMetadata() {
+ return metadata;
+ }
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ public static class Builder {
+ private @Nullable Boolean streaming;
+ private @Nullable Boolean polling;
+ private @Nullable Boolean useClientPreference;
+ private List acceptedOutputModes = new ArrayList<>();
+ private @Nullable PushNotificationConfig_v0_3 pushNotificationConfig;
+ private @Nullable Integer historyLength;
+ private Map metadata = new HashMap<>();
+
+ public Builder setStreaming(@Nullable Boolean streaming) {
+ this.streaming = streaming;
+ return this;
+ }
+
+ public Builder setPolling(@Nullable Boolean polling) {
+ this.polling = polling;
+ return this;
+ }
+
+ public Builder setUseClientPreference(@Nullable Boolean useClientPreference) {
+ this.useClientPreference = useClientPreference;
+ return this;
+ }
+
+ public Builder setAcceptedOutputModes(List acceptedOutputModes) {
+ this.acceptedOutputModes = new ArrayList<>(acceptedOutputModes);
+ return this;
+ }
+
+ public Builder setPushNotificationConfig(PushNotificationConfig_v0_3 pushNotificationConfig) {
+ this.pushNotificationConfig = pushNotificationConfig;
+ return this;
+ }
+
+ public Builder setHistoryLength(Integer historyLength) {
+ this.historyLength = historyLength;
+ return this;
+ }
+
+ public Builder setMetadata(Map metadata) {
+ this.metadata = metadata;
+ return this;
+ }
+
+ public ClientConfig_v0_3 build() {
+ return new ClientConfig_v0_3(this);
+ }
+ }
+}
\ No newline at end of file
diff --git a/compat-0.3/client/base/src/main/java/org/a2aproject/sdk/compat03/client/config/package-info.java b/compat-0.3/client/base/src/main/java/org/a2aproject/sdk/compat03/client/config/package-info.java
new file mode 100644
index 000000000..bfae93f4b
--- /dev/null
+++ b/compat-0.3/client/base/src/main/java/org/a2aproject/sdk/compat03/client/config/package-info.java
@@ -0,0 +1,5 @@
+@NullMarked
+package org.a2aproject.sdk.compat03.client.config;
+
+import org.jspecify.annotations.NullMarked;
+
diff --git a/compat-0.3/client/base/src/main/java/org/a2aproject/sdk/compat03/client/package-info.java b/compat-0.3/client/base/src/main/java/org/a2aproject/sdk/compat03/client/package-info.java
new file mode 100644
index 000000000..9bd22f637
--- /dev/null
+++ b/compat-0.3/client/base/src/main/java/org/a2aproject/sdk/compat03/client/package-info.java
@@ -0,0 +1,5 @@
+@NullMarked
+package org.a2aproject.sdk.compat03.client;
+
+import org.jspecify.annotations.NullMarked;
+
diff --git a/compat-0.3/client/base/src/main/resources/META-INF/beans.xml b/compat-0.3/client/base/src/main/resources/META-INF/beans.xml
new file mode 100644
index 000000000..e69de29bb
diff --git a/compat-0.3/client/base/src/test/java/org/a2aproject/sdk/compat03/A2A_v0_3_Test.java b/compat-0.3/client/base/src/test/java/org/a2aproject/sdk/compat03/A2A_v0_3_Test.java
new file mode 100644
index 000000000..1187518eb
--- /dev/null
+++ b/compat-0.3/client/base/src/test/java/org/a2aproject/sdk/compat03/A2A_v0_3_Test.java
@@ -0,0 +1,147 @@
+package org.a2aproject.sdk.compat03;
+
+import org.a2aproject.sdk.compat03.spec.Message_v0_3;
+import org.a2aproject.sdk.compat03.spec.Part_v0_3;
+import org.a2aproject.sdk.compat03.spec.TextPart_v0_3;
+import org.junit.jupiter.api.Test;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+public class A2A_v0_3_Test {
+
+ @Test
+ public void testToUserMessage() {
+ String text = "Hello, world!";
+ Message_v0_3 message = A2A_v0_3.toUserMessage(text);
+
+ assertEquals(Message_v0_3.Role.USER, message.getRole());
+ assertEquals(1, message.getParts().size());
+ assertEquals(text, ((TextPart_v0_3) message.getParts().get(0)).getText());
+ assertNotNull(message.getMessageId());
+ assertNull(message.getContextId());
+ assertNull(message.getTaskId());
+ }
+
+ @Test
+ public void testToUserMessageWithId() {
+ String text = "Hello, world!";
+ String messageId = "test-message-id";
+ Message_v0_3 message = A2A_v0_3.toUserMessage(text, messageId);
+
+ assertEquals(Message_v0_3.Role.USER, message.getRole());
+ assertEquals(messageId, message.getMessageId());
+ }
+
+ @Test
+ public void testToAgentMessage() {
+ String text = "Hello, I'm an agent!";
+ Message_v0_3 message = A2A_v0_3.toAgentMessage(text);
+
+ assertEquals(Message_v0_3.Role.AGENT, message.getRole());
+ assertEquals(1, message.getParts().size());
+ assertEquals(text, ((TextPart_v0_3) message.getParts().get(0)).getText());
+ assertNotNull(message.getMessageId());
+ }
+
+ @Test
+ public void testToAgentMessageWithId() {
+ String text = "Hello, I'm an agent!";
+ String messageId = "agent-message-id";
+ Message_v0_3 message = A2A_v0_3.toAgentMessage(text, messageId);
+
+ assertEquals(Message_v0_3.Role.AGENT, message.getRole());
+ assertEquals(messageId, message.getMessageId());
+ }
+
+ @Test
+ public void testCreateUserTextMessage() {
+ String text = "User message with context";
+ String contextId = "context-123";
+ String taskId = "task-456";
+
+ Message_v0_3 message = A2A_v0_3.createUserTextMessage(text, contextId, taskId);
+
+ assertEquals(Message_v0_3.Role.USER, message.getRole());
+ assertEquals(contextId, message.getContextId());
+ assertEquals(taskId, message.getTaskId());
+ assertEquals(1, message.getParts().size());
+ assertEquals(text, ((TextPart_v0_3) message.getParts().get(0)).getText());
+ assertNotNull(message.getMessageId());
+ assertNull(message.getMetadata());
+ assertNull(message.getReferenceTaskIds());
+ }
+
+ @Test
+ public void testCreateUserTextMessageWithNullParams() {
+ String text = "Simple user message";
+
+ Message_v0_3 message = A2A_v0_3.createUserTextMessage(text, null, null);
+
+ assertEquals(Message_v0_3.Role.USER, message.getRole());
+ assertNull(message.getContextId());
+ assertNull(message.getTaskId());
+ assertEquals(1, message.getParts().size());
+ assertEquals(text, ((TextPart_v0_3) message.getParts().get(0)).getText());
+ }
+
+ @Test
+ public void testCreateAgentTextMessage() {
+ String text = "Agent message with context";
+ String contextId = "context-789";
+ String taskId = "task-012";
+
+ Message_v0_3 message = A2A_v0_3.createAgentTextMessage(text, contextId, taskId);
+
+ assertEquals(Message_v0_3.Role.AGENT, message.getRole());
+ assertEquals(contextId, message.getContextId());
+ assertEquals(taskId, message.getTaskId());
+ assertEquals(1, message.getParts().size());
+ assertEquals(text, ((TextPart_v0_3) message.getParts().get(0)).getText());
+ assertNotNull(message.getMessageId());
+ }
+
+ @Test
+ public void testCreateAgentPartsMessage() {
+ List> parts = Arrays.asList(
+ new TextPart_v0_3("Part 1"),
+ new TextPart_v0_3("Part 2")
+ );
+ String contextId = "context-parts";
+ String taskId = "task-parts";
+
+ Message_v0_3 message = A2A_v0_3.createAgentPartsMessage(parts, contextId, taskId);
+
+ assertEquals(Message_v0_3.Role.AGENT, message.getRole());
+ assertEquals(contextId, message.getContextId());
+ assertEquals(taskId, message.getTaskId());
+ assertEquals(2, message.getParts().size());
+ assertEquals("Part 1", ((TextPart_v0_3) message.getParts().get(0)).getText());
+ assertEquals("Part 2", ((TextPart_v0_3) message.getParts().get(1)).getText());
+ }
+
+ @Test
+ public void testCreateAgentPartsMessageWithNullParts() {
+ try {
+ A2A_v0_3.createAgentPartsMessage(null, "context", "task");
+ org.junit.jupiter.api.Assertions.fail("Expected IllegalArgumentException");
+ } catch (IllegalArgumentException e) {
+ assertEquals("Parts cannot be null or empty", e.getMessage());
+ }
+ }
+
+ @Test
+ public void testCreateAgentPartsMessageWithEmptyParts() {
+ try {
+ A2A_v0_3.createAgentPartsMessage(Collections.emptyList(), "context", "task");
+ org.junit.jupiter.api.Assertions.fail("Expected IllegalArgumentException");
+ } catch (IllegalArgumentException e) {
+ assertEquals("Parts cannot be null or empty", e.getMessage());
+ }
+ }
+}
\ No newline at end of file
diff --git a/compat-0.3/client/base/src/test/java/org/a2aproject/sdk/compat03/client/AuthenticationAuthorization_v0_3_Test.java b/compat-0.3/client/base/src/test/java/org/a2aproject/sdk/compat03/client/AuthenticationAuthorization_v0_3_Test.java
new file mode 100644
index 000000000..228a08c09
--- /dev/null
+++ b/compat-0.3/client/base/src/test/java/org/a2aproject/sdk/compat03/client/AuthenticationAuthorization_v0_3_Test.java
@@ -0,0 +1,380 @@
+package org.a2aproject.sdk.compat03.client;
+
+import org.a2aproject.sdk.compat03.client.config.ClientConfig_v0_3;
+import org.a2aproject.sdk.compat03.client.transport.grpc.GrpcTransport_v0_3;
+import org.a2aproject.sdk.compat03.client.transport.grpc.GrpcTransportConfigBuilder_v0_3;
+import org.a2aproject.sdk.compat03.client.transport.jsonrpc.JSONRPCTransport_v0_3;
+import org.a2aproject.sdk.compat03.client.transport.jsonrpc.JSONRPCTransportConfigBuilder_v0_3;
+import org.a2aproject.sdk.compat03.client.transport.rest.RestTransport_v0_3;
+import org.a2aproject.sdk.compat03.client.transport.rest.RestTransportConfigBuilder_v0_3;
+import org.a2aproject.sdk.compat03.grpc.A2AServiceGrpc;
+import org.a2aproject.sdk.compat03.grpc.SendMessageRequest;
+import org.a2aproject.sdk.compat03.grpc.SendMessageResponse;
+import org.a2aproject.sdk.compat03.grpc.StreamResponse;
+import org.a2aproject.sdk.compat03.spec.A2AClientException_v0_3;
+import org.a2aproject.sdk.compat03.spec.AgentCapabilities_v0_3;
+import org.a2aproject.sdk.compat03.spec.AgentCard_v0_3;
+import org.a2aproject.sdk.compat03.spec.AgentInterface_v0_3;
+import org.a2aproject.sdk.compat03.spec.AgentSkill_v0_3;
+import org.a2aproject.sdk.compat03.spec.Message_v0_3;
+import org.a2aproject.sdk.compat03.spec.TextPart_v0_3;
+import org.a2aproject.sdk.compat03.spec.TransportProtocol_v0_3;
+import io.grpc.ManagedChannel;
+import io.grpc.Server;
+import io.grpc.Status;
+import io.grpc.inprocess.InProcessChannelBuilder;
+import io.grpc.inprocess.InProcessServerBuilder;
+import io.grpc.stub.StreamObserver;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockserver.integration.ClientAndServer;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Consumer;
+
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockserver.model.HttpRequest.request;
+import static org.mockserver.model.HttpResponse.response;
+
+/**
+ * Tests for handling HTTP 401 (Unauthorized) and 403 (Forbidden) responses
+ * when the client sends streaming and non-streaming messages.
+ *
+ * These tests verify that the client properly fails when the server returns
+ * authentication or authorization errors.
+ */
+public class AuthenticationAuthorization_v0_3_Test {
+
+ private static final String AGENT_URL = "http://localhost:4001";
+ private static final String AUTHENTICATION_FAILED_MESSAGE = "Authentication failed";
+ private static final String AUTHORIZATION_FAILED_MESSAGE = "Authorization failed";
+
+ private ClientAndServer server;
+ private Message_v0_3 MESSAGE;
+ private AgentCard_v0_3 agentCard;
+ private Server grpcServer;
+ private ManagedChannel grpcChannel;
+ private String grpcServerName;
+
+ @BeforeEach
+ public void setUp() {
+ server = new ClientAndServer(4001);
+ MESSAGE = new Message_v0_3.Builder()
+ .role(Message_v0_3.Role.USER)
+ .parts(Collections.singletonList(new TextPart_v0_3("test message")))
+ .contextId("context-1234")
+ .messageId("message-1234")
+ .build();
+
+ grpcServerName = InProcessServerBuilder.generateName();
+
+ agentCard = new AgentCard_v0_3.Builder()
+ .name("Test Agent")
+ .description("Test agent for auth tests")
+ .url(AGENT_URL)
+ .version("1.0.0")
+ .capabilities(new AgentCapabilities_v0_3.Builder()
+ .streaming(true) // Support streaming for all tests
+ .build())
+ .defaultInputModes(Collections.singletonList("text"))
+ .defaultOutputModes(Collections.singletonList("text"))
+ .skills(Collections.singletonList(new AgentSkill_v0_3.Builder()
+ .id("test_skill")
+ .name("Test skill")
+ .description("Test skill")
+ .tags(Collections.singletonList("test"))
+ .build()))
+ .protocolVersion("0.3.0")
+ .additionalInterfaces(java.util.Arrays.asList(
+ new AgentInterface_v0_3(TransportProtocol_v0_3.JSONRPC.asString(), AGENT_URL),
+ new AgentInterface_v0_3(TransportProtocol_v0_3.HTTP_JSON.asString(), AGENT_URL),
+ new AgentInterface_v0_3(TransportProtocol_v0_3.GRPC.asString(), grpcServerName)))
+ .build();
+ }
+
+ @AfterEach
+ public void tearDown() {
+ server.stop();
+ if (grpcChannel != null) {
+ grpcChannel.shutdownNow();
+ }
+ if (grpcServer != null) {
+ grpcServer.shutdownNow();
+ }
+ }
+
+ // ========== JSON-RPC Transport Tests ==========
+
+ @Test
+ public void testJsonRpcNonStreamingUnauthenticated() throws A2AClientException_v0_3 {
+ // Mock server to return 401 for non-streaming message
+ server.when(
+ request()
+ .withMethod("POST")
+ .withPath("/")
+ ).respond(
+ response()
+ .withStatusCode(401)
+ );
+
+ Client_v0_3 client = getJSONRPCClientBuilder(false).build();
+
+ A2AClientException_v0_3 exception = assertThrows(A2AClientException_v0_3.class, () -> {
+ client.sendMessage(MESSAGE);
+ });
+
+ assertTrue(exception.getMessage().contains(AUTHENTICATION_FAILED_MESSAGE));
+ }
+
+ @Test
+ public void testJsonRpcNonStreamingUnauthorized() throws A2AClientException_v0_3 {
+ // Mock server to return 403 for non-streaming message
+ server.when(
+ request()
+ .withMethod("POST")
+ .withPath("/")
+ ).respond(
+ response()
+ .withStatusCode(403)
+ );
+
+ Client_v0_3 client = getJSONRPCClientBuilder(false).build();
+
+ A2AClientException_v0_3 exception = assertThrows(A2AClientException_v0_3.class, () -> {
+ client.sendMessage(MESSAGE);
+ });
+
+ assertTrue(exception.getMessage().contains(AUTHORIZATION_FAILED_MESSAGE));
+ }
+
+ @Test
+ public void testJsonRpcStreamingUnauthenticated() throws Exception {
+ // Mock server to return 401 for streaming message
+ server.when(
+ request()
+ .withMethod("POST")
+ .withPath("/")
+ ).respond(
+ response()
+ .withStatusCode(401)
+ );
+
+ assertStreamingError(
+ getJSONRPCClientBuilder(true),
+ AUTHENTICATION_FAILED_MESSAGE);
+ }
+
+ @Test
+ public void testJsonRpcStreamingUnauthorized() throws Exception {
+ // Mock server to return 403 for streaming message
+ server.when(
+ request()
+ .withMethod("POST")
+ .withPath("/")
+ ).respond(
+ response()
+ .withStatusCode(403)
+ );
+
+ assertStreamingError(
+ getJSONRPCClientBuilder(true),
+ AUTHORIZATION_FAILED_MESSAGE);
+ }
+
+ // ========== REST Transport Tests ==========
+
+ @Test
+ public void testRestNonStreamingUnauthenticated() throws A2AClientException_v0_3 {
+ // Mock server to return 401 for non-streaming message
+ server.when(
+ request()
+ .withMethod("POST")
+ .withPath("/v1/message:send")
+ ).respond(
+ response()
+ .withStatusCode(401)
+ );
+
+ Client_v0_3 client = getRestClientBuilder(false).build();
+
+ A2AClientException_v0_3 exception = assertThrows(A2AClientException_v0_3.class, () -> {
+ client.sendMessage(MESSAGE);
+ });
+
+ assertTrue(exception.getMessage().contains(AUTHENTICATION_FAILED_MESSAGE));
+ }
+
+ @Test
+ public void testRestNonStreamingUnauthorized() throws A2AClientException_v0_3 {
+ // Mock server to return 403 for non-streaming message
+ server.when(
+ request()
+ .withMethod("POST")
+ .withPath("/v1/message:send")
+ ).respond(
+ response()
+ .withStatusCode(403)
+ );
+
+ Client_v0_3 client = getRestClientBuilder(false).build();
+
+ A2AClientException_v0_3 exception = assertThrows(A2AClientException_v0_3.class, () -> {
+ client.sendMessage(MESSAGE);
+ });
+
+ assertTrue(exception.getMessage().contains(AUTHORIZATION_FAILED_MESSAGE));
+ }
+
+ @Test
+ public void testRestStreamingUnauthenticated() throws Exception {
+ // Mock server to return 401 for streaming message
+ server.when(
+ request()
+ .withMethod("POST")
+ .withPath("/v1/message:stream")
+ ).respond(
+ response()
+ .withStatusCode(401)
+ );
+
+ assertStreamingError(
+ getRestClientBuilder(true),
+ AUTHENTICATION_FAILED_MESSAGE);
+ }
+
+ @Test
+ public void testRestStreamingUnauthorized() throws Exception {
+ // Mock server to return 403 for streaming message
+ server.when(
+ request()
+ .withMethod("POST")
+ .withPath("/v1/message:stream")
+ ).respond(
+ response()
+ .withStatusCode(403)
+ );
+
+ assertStreamingError(
+ getRestClientBuilder(true),
+ AUTHORIZATION_FAILED_MESSAGE);
+ }
+
+ // ========== gRPC Transport Tests ==========
+
+ @Test
+ public void testGrpcNonStreamingUnauthenticated() throws Exception {
+ setupGrpcServer(Status.UNAUTHENTICATED);
+
+ Client_v0_3 client = getGrpcClientBuilder(false).build();
+
+ A2AClientException_v0_3 exception = assertThrows(A2AClientException_v0_3.class, () -> {
+ client.sendMessage(MESSAGE);
+ });
+
+ assertTrue(exception.getMessage().contains(AUTHENTICATION_FAILED_MESSAGE));
+ }
+
+ @Test
+ public void testGrpcNonStreamingUnauthorized() throws Exception {
+ setupGrpcServer(Status.PERMISSION_DENIED);
+
+ Client_v0_3 client = getGrpcClientBuilder(false).build();
+
+ A2AClientException_v0_3 exception = assertThrows(A2AClientException_v0_3.class, () -> {
+ client.sendMessage(MESSAGE);
+ });
+
+ assertTrue(exception.getMessage().contains(AUTHORIZATION_FAILED_MESSAGE));
+ }
+
+ @Test
+ public void testGrpcStreamingUnauthenticated() throws Exception {
+ setupGrpcServer(Status.UNAUTHENTICATED);
+
+ assertStreamingError(
+ getGrpcClientBuilder(true),
+ AUTHENTICATION_FAILED_MESSAGE);
+ }
+
+ @Test
+ public void testGrpcStreamingUnauthorized() throws Exception {
+ setupGrpcServer(Status.PERMISSION_DENIED);
+
+ assertStreamingError(
+ getGrpcClientBuilder(true),
+ AUTHORIZATION_FAILED_MESSAGE);
+ }
+
+ private ClientBuilder_v0_3 getJSONRPCClientBuilder(boolean streaming) {
+ return Client_v0_3.builder(agentCard)
+ .clientConfig(new ClientConfig_v0_3.Builder().setStreaming(streaming).build())
+ .withTransport(JSONRPCTransport_v0_3.class, new JSONRPCTransportConfigBuilder_v0_3());
+ }
+
+ private ClientBuilder_v0_3 getRestClientBuilder(boolean streaming) {
+ return Client_v0_3.builder(agentCard)
+ .clientConfig(new ClientConfig_v0_3.Builder().setStreaming(streaming).build())
+ .withTransport(RestTransport_v0_3.class, new RestTransportConfigBuilder_v0_3());
+ }
+
+ private ClientBuilder_v0_3 getGrpcClientBuilder(boolean streaming) {
+ return Client_v0_3.builder(agentCard)
+ .clientConfig(new ClientConfig_v0_3.Builder().setStreaming(streaming).build())
+ .withTransport(GrpcTransport_v0_3.class, new GrpcTransportConfigBuilder_v0_3()
+ .channelFactory(target -> grpcChannel));
+ }
+
+ private void assertStreamingError(ClientBuilder_v0_3 clientBuilder, String expectedErrorMessage) throws Exception {
+ AtomicReference errorRef = new AtomicReference<>();
+ CountDownLatch errorLatch = new CountDownLatch(1);
+
+ Consumer errorHandler = error -> {
+ errorRef.set(error);
+ errorLatch.countDown();
+ };
+
+ Client_v0_3 client = clientBuilder.streamingErrorHandler(errorHandler).build();
+
+ try {
+ client.sendMessage(MESSAGE);
+ // If no immediate exception, wait for async error
+ assertTrue(errorLatch.await(5, TimeUnit.SECONDS), "Expected error handler to be called");
+ Throwable error = errorRef.get();
+ assertTrue(error.getMessage().contains(expectedErrorMessage),
+ "Expected error message to contain '" + expectedErrorMessage + "' but got: " + error.getMessage());
+ } catch (Exception e) {
+ // Immediate exception is also acceptable
+ assertTrue(e.getMessage().contains(expectedErrorMessage),
+ "Expected error message to contain '" + expectedErrorMessage + "' but got: " + e.getMessage());
+ }
+ }
+
+ private void setupGrpcServer(Status status) throws IOException {
+ grpcServerName = InProcessServerBuilder.generateName();
+ grpcServer = InProcessServerBuilder.forName(grpcServerName)
+ .directExecutor()
+ .addService(new A2AServiceGrpc.A2AServiceImplBase() {
+ @Override
+ public void sendMessage(SendMessageRequest request, StreamObserver responseObserver) {
+ responseObserver.onError(status.asRuntimeException());
+ }
+
+ @Override
+ public void sendStreamingMessage(SendMessageRequest request, StreamObserver responseObserver) {
+ responseObserver.onError(status.asRuntimeException());
+ }
+ })
+ .build()
+ .start();
+
+ grpcChannel = InProcessChannelBuilder.forName(grpcServerName)
+ .directExecutor()
+ .build();
+ }
+}
\ No newline at end of file
diff --git a/compat-0.3/client/base/src/test/java/org/a2aproject/sdk/compat03/client/ClientBuilder_v0_3_Test.java b/compat-0.3/client/base/src/test/java/org/a2aproject/sdk/compat03/client/ClientBuilder_v0_3_Test.java
new file mode 100644
index 000000000..4b4e31d0b
--- /dev/null
+++ b/compat-0.3/client/base/src/test/java/org/a2aproject/sdk/compat03/client/ClientBuilder_v0_3_Test.java
@@ -0,0 +1,96 @@
+package org.a2aproject.sdk.compat03.client;
+
+import org.a2aproject.sdk.compat03.client.config.ClientConfig_v0_3;
+import org.a2aproject.sdk.compat03.client.http.JdkA2AHttpClient_v0_3;
+import org.a2aproject.sdk.compat03.client.transport.grpc.GrpcTransport_v0_3;
+import org.a2aproject.sdk.compat03.client.transport.grpc.GrpcTransportConfigBuilder_v0_3;
+import org.a2aproject.sdk.compat03.client.transport.jsonrpc.JSONRPCTransport_v0_3;
+import org.a2aproject.sdk.compat03.client.transport.jsonrpc.JSONRPCTransportConfig_v0_3;
+import org.a2aproject.sdk.compat03.client.transport.jsonrpc.JSONRPCTransportConfigBuilder_v0_3;
+import org.a2aproject.sdk.compat03.spec.A2AClientException_v0_3;
+import org.a2aproject.sdk.compat03.spec.AgentCapabilities_v0_3;
+import org.a2aproject.sdk.compat03.spec.AgentCard_v0_3;
+import org.a2aproject.sdk.compat03.spec.AgentInterface_v0_3;
+import org.a2aproject.sdk.compat03.spec.AgentSkill_v0_3;
+import org.a2aproject.sdk.compat03.spec.TransportProtocol_v0_3;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import java.util.Collections;
+import java.util.List;
+
+public class ClientBuilder_v0_3_Test {
+
+ private AgentCard_v0_3 card = new AgentCard_v0_3.Builder()
+ .name("Hello World Agent")
+ .description("Just a hello world agent")
+ .url("http://localhost:9999")
+ .version("1.0.0")
+ .documentationUrl("http://example.com/docs")
+ .capabilities(new AgentCapabilities_v0_3.Builder()
+ .streaming(true)
+ .pushNotifications(true)
+ .stateTransitionHistory(true)
+ .build())
+ .defaultInputModes(Collections.singletonList("text"))
+ .defaultOutputModes(Collections.singletonList("text"))
+ .skills(Collections.singletonList(new AgentSkill_v0_3.Builder()
+ .id("hello_world")
+ .name("Returns hello world")
+ .description("just returns hello world")
+ .tags(Collections.singletonList("hello world"))
+ .examples(List.of("hi", "hello world"))
+ .build()))
+ .protocolVersion("0.3.0")
+ .additionalInterfaces(List.of(
+ new AgentInterface_v0_3(TransportProtocol_v0_3.JSONRPC.asString(), "http://localhost:9999")))
+ .build();
+
+ @Test
+ public void shouldNotFindCompatibleTransport() throws A2AClientException_v0_3 {
+ A2AClientException_v0_3 exception = Assertions.assertThrows(A2AClientException_v0_3.class,
+ () -> Client_v0_3
+ .builder(card)
+ .clientConfig(new ClientConfig_v0_3.Builder().setUseClientPreference(true).build())
+ .withTransport(GrpcTransport_v0_3.class, new GrpcTransportConfigBuilder_v0_3()
+ .channelFactory(s -> null))
+ .build());
+
+ Assertions.assertTrue(exception.getMessage() != null && exception.getMessage().contains("No compatible transport found"));
+ }
+
+ @Test
+ public void shouldNotFindConfigurationTransport() throws A2AClientException_v0_3 {
+ A2AClientException_v0_3 exception = Assertions.assertThrows(A2AClientException_v0_3.class,
+ () -> Client_v0_3
+ .builder(card)
+ .clientConfig(new ClientConfig_v0_3.Builder().setUseClientPreference(true).build())
+ .build());
+
+ Assertions.assertTrue(exception.getMessage() != null && exception.getMessage().startsWith("Missing required TransportConfig for"));
+ }
+
+ @Test
+ public void shouldCreateJSONRPCClient() throws A2AClientException_v0_3 {
+ Client_v0_3 client = Client_v0_3
+ .builder(card)
+ .clientConfig(new ClientConfig_v0_3.Builder().setUseClientPreference(true).build())
+ .withTransport(JSONRPCTransport_v0_3.class, new JSONRPCTransportConfigBuilder_v0_3()
+ .addInterceptor(null)
+ .httpClient(null))
+ .build();
+
+ Assertions.assertNotNull(client);
+ }
+
+ @Test
+ public void shouldCreateClient_differentConfigurations() throws A2AClientException_v0_3 {
+ Client_v0_3 client = Client_v0_3
+ .builder(card)
+ .withTransport(JSONRPCTransport_v0_3.class, new JSONRPCTransportConfigBuilder_v0_3())
+ .withTransport(JSONRPCTransport_v0_3.class, new JSONRPCTransportConfig_v0_3(new JdkA2AHttpClient_v0_3()))
+ .build();
+
+ Assertions.assertNotNull(client);
+ }
+}
diff --git a/compat-0.3/client/transport/grpc/pom.xml b/compat-0.3/client/transport/grpc/pom.xml
new file mode 100644
index 000000000..97c24e11a
--- /dev/null
+++ b/compat-0.3/client/transport/grpc/pom.xml
@@ -0,0 +1,57 @@
+
+
+ 4.0.0
+
+
+ org.a2aproject.sdk
+ a2a-java-sdk-compat-0.3-parent
+ 1.0.0.Beta2-SNAPSHOT
+ ../../..
+
+ a2a-java-sdk-compat-0.3-client-transport-grpc
+ jar
+
+ Java SDK A2A Compat 0.3 Client Transport: gRPC
+ Java SDK for the Agent2Agent Protocol (A2A) - gRPC Client Transport
+
+
+
+ ${project.groupId}
+ a2a-java-sdk-common
+
+
+ ${project.groupId}
+ a2a-java-sdk-compat-0.3-spec
+
+
+ ${project.groupId}
+ a2a-java-sdk-compat-0.3-spec-grpc
+
+
+ ${project.groupId}
+ a2a-java-sdk-compat-0.3-client-transport-spi
+
+
+ io.grpc
+ grpc-protobuf
+
+
+ io.grpc
+ grpc-stub
+
+
+ org.junit.jupiter
+ junit-jupiter-api
+ test
+
+
+
+ org.mock-server
+ mockserver-netty
+ test
+
+
+
+
\ No newline at end of file
diff --git a/compat-0.3/client/transport/grpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/grpc/EventStreamObserver_v0_3.java b/compat-0.3/client/transport/grpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/grpc/EventStreamObserver_v0_3.java
new file mode 100644
index 000000000..4f353748c
--- /dev/null
+++ b/compat-0.3/client/transport/grpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/grpc/EventStreamObserver_v0_3.java
@@ -0,0 +1,64 @@
+package org.a2aproject.sdk.compat03.client.transport.grpc;
+
+
+import org.a2aproject.sdk.compat03.grpc.StreamResponse;
+import org.a2aproject.sdk.compat03.spec.StreamingEventKind_v0_3;
+import io.grpc.stub.StreamObserver;
+
+import java.util.function.Consumer;
+import java.util.logging.Logger;
+
+import static org.a2aproject.sdk.compat03.grpc.utils.ProtoUtils_v0_3.FromProto;
+
+public class EventStreamObserver_v0_3 implements StreamObserver {
+
+ private static final Logger log = Logger.getLogger(EventStreamObserver_v0_3.class.getName());
+ private final Consumer eventHandler;
+ private final Consumer errorHandler;
+
+ public EventStreamObserver_v0_3(Consumer eventHandler, Consumer errorHandler) {
+ this.eventHandler = eventHandler;
+ this.errorHandler = errorHandler;
+ }
+
+ @Override
+ public void onNext(StreamResponse response) {
+ StreamingEventKind_v0_3 event;
+ switch (response.getPayloadCase()) {
+ case MSG:
+ event = FromProto.message(response.getMsg());
+ break;
+ case TASK:
+ event = FromProto.task(response.getTask());
+ break;
+ case STATUS_UPDATE:
+ event = FromProto.taskStatusUpdateEvent(response.getStatusUpdate());
+ break;
+ case ARTIFACT_UPDATE:
+ event = FromProto.taskArtifactUpdateEvent(response.getArtifactUpdate());
+ break;
+ default:
+ log.warning("Invalid stream response " + response.getPayloadCase());
+ errorHandler.accept(new IllegalStateException("Invalid stream response from server: " + response.getPayloadCase()));
+ return;
+ }
+ eventHandler.accept(event);
+ }
+
+ @Override
+ public void onError(Throwable t) {
+ if (errorHandler != null) {
+ // Map gRPC errors to proper A2A exceptions
+ if (t instanceof io.grpc.StatusRuntimeException) {
+ errorHandler.accept(GrpcErrorMapper_v0_3.mapGrpcError((io.grpc.StatusRuntimeException) t));
+ } else {
+ errorHandler.accept(t);
+ }
+ }
+ }
+
+ @Override
+ public void onCompleted() {
+ // done
+ }
+}
diff --git a/compat-0.3/client/transport/grpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/grpc/GrpcErrorMapper_v0_3.java b/compat-0.3/client/transport/grpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/grpc/GrpcErrorMapper_v0_3.java
new file mode 100644
index 000000000..a66bbf610
--- /dev/null
+++ b/compat-0.3/client/transport/grpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/grpc/GrpcErrorMapper_v0_3.java
@@ -0,0 +1,102 @@
+package org.a2aproject.sdk.compat03.client.transport.grpc;
+
+import org.a2aproject.sdk.common.A2AErrorMessages;
+import org.a2aproject.sdk.compat03.spec.A2AClientException_v0_3;
+import org.a2aproject.sdk.compat03.spec.ContentTypeNotSupportedError_v0_3;
+import org.a2aproject.sdk.compat03.spec.InternalError_v0_3;
+import org.a2aproject.sdk.compat03.spec.InvalidAgentResponseError_v0_3;
+import org.a2aproject.sdk.compat03.spec.InvalidParamsError_v0_3;
+import org.a2aproject.sdk.compat03.spec.InvalidRequestError_v0_3;
+import org.a2aproject.sdk.compat03.spec.JSONParseError_v0_3;
+import org.a2aproject.sdk.compat03.spec.MethodNotFoundError_v0_3;
+import org.a2aproject.sdk.compat03.spec.PushNotificationNotSupportedError_v0_3;
+import org.a2aproject.sdk.compat03.spec.TaskNotCancelableError_v0_3;
+import org.a2aproject.sdk.compat03.spec.TaskNotFoundError_v0_3;
+import org.a2aproject.sdk.compat03.spec.UnsupportedOperationError_v0_3;
+import io.grpc.Status;
+import io.grpc.StatusException;
+import io.grpc.StatusRuntimeException;
+
+/**
+ * Utility class to map gRPC StatusRuntimeException to appropriate A2A error types
+ */
+public class GrpcErrorMapper_v0_3 {
+
+ // Overload for StatusRuntimeException (original 0.3.x signature)
+ public static A2AClientException_v0_3 mapGrpcError(StatusRuntimeException e) {
+ return mapGrpcError(e, "gRPC error: ");
+ }
+
+ public static A2AClientException_v0_3 mapGrpcError(StatusRuntimeException e, String errorPrefix) {
+ return mapGrpcErrorInternal(e.getStatus().getCode(), e.getStatus().getDescription(), e, errorPrefix);
+ }
+
+ // Overload for StatusException (gRPC 1.77+ compatibility)
+ public static A2AClientException_v0_3 mapGrpcError(StatusException e) {
+ return mapGrpcError(e, "gRPC error: ");
+ }
+
+ public static A2AClientException_v0_3 mapGrpcError(StatusException e, String errorPrefix) {
+ return mapGrpcErrorInternal(e.getStatus().getCode(), e.getStatus().getDescription(), e, errorPrefix);
+ }
+
+ // Dispatcher for multi-catch (StatusRuntimeException | StatusException)
+ public static A2AClientException_v0_3 mapGrpcError(Exception e, String errorPrefix) {
+ if (e instanceof StatusRuntimeException) {
+ return mapGrpcError((StatusRuntimeException) e, errorPrefix);
+ } else if (e instanceof StatusException) {
+ return mapGrpcError((StatusException) e, errorPrefix);
+ } else {
+ return new A2AClientException_v0_3(errorPrefix + e.getMessage(), e);
+ }
+ }
+
+ private static A2AClientException_v0_3 mapGrpcErrorInternal(Status.Code code, @org.jspecify.annotations.Nullable String description, @org.jspecify.annotations.Nullable Throwable cause, String errorPrefix) {
+
+ // Extract the actual error type from the description if possible
+ // (using description because the same code can map to multiple errors -
+ // see GrpcHandler#handleError)
+ if (description != null) {
+ if (description.contains("TaskNotFoundError")) {
+ return new A2AClientException_v0_3(errorPrefix + description, new TaskNotFoundError_v0_3());
+ } else if (description.contains("UnsupportedOperationError")) {
+ return new A2AClientException_v0_3(errorPrefix + description, new UnsupportedOperationError_v0_3());
+ } else if (description.contains("InvalidParamsError")) {
+ return new A2AClientException_v0_3(errorPrefix + description, new InvalidParamsError_v0_3());
+ } else if (description.contains("InvalidRequestError")) {
+ return new A2AClientException_v0_3(errorPrefix + description, new InvalidRequestError_v0_3());
+ } else if (description.contains("MethodNotFoundError")) {
+ return new A2AClientException_v0_3(errorPrefix + description, new MethodNotFoundError_v0_3());
+ } else if (description.contains("TaskNotCancelableError")) {
+ return new A2AClientException_v0_3(errorPrefix + description, new TaskNotCancelableError_v0_3());
+ } else if (description.contains("PushNotificationNotSupportedError")) {
+ return new A2AClientException_v0_3(errorPrefix + description, new PushNotificationNotSupportedError_v0_3());
+ } else if (description.contains("JSONParseError")) {
+ return new A2AClientException_v0_3(errorPrefix + description, new JSONParseError_v0_3());
+ } else if (description.contains("ContentTypeNotSupportedError")) {
+ return new A2AClientException_v0_3(errorPrefix + description, new ContentTypeNotSupportedError_v0_3(null, description, null));
+ } else if (description.contains("InvalidAgentResponseError")) {
+ return new A2AClientException_v0_3(errorPrefix + description, new InvalidAgentResponseError_v0_3(null, description, null));
+ }
+ }
+
+ // Fall back to mapping based on status code
+ String message = description != null ? description : (cause != null ? cause.getMessage() : "Unknown error");
+ switch (code) {
+ case NOT_FOUND:
+ return new A2AClientException_v0_3(errorPrefix + message, new TaskNotFoundError_v0_3());
+ case UNIMPLEMENTED:
+ return new A2AClientException_v0_3(errorPrefix + message, new UnsupportedOperationError_v0_3());
+ case INVALID_ARGUMENT:
+ return new A2AClientException_v0_3(errorPrefix + message, new InvalidParamsError_v0_3());
+ case INTERNAL:
+ return new A2AClientException_v0_3(errorPrefix + message, new InternalError_v0_3(null, message, null));
+ case UNAUTHENTICATED:
+ return new A2AClientException_v0_3(errorPrefix + A2AErrorMessages.AUTHENTICATION_FAILED);
+ case PERMISSION_DENIED:
+ return new A2AClientException_v0_3(errorPrefix + A2AErrorMessages.AUTHORIZATION_FAILED);
+ default:
+ return new A2AClientException_v0_3(errorPrefix + message, cause);
+ }
+ }
+}
diff --git a/compat-0.3/client/transport/grpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/grpc/GrpcTransportConfigBuilder_v0_3.java b/compat-0.3/client/transport/grpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/grpc/GrpcTransportConfigBuilder_v0_3.java
new file mode 100644
index 000000000..cfc3b97c6
--- /dev/null
+++ b/compat-0.3/client/transport/grpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/grpc/GrpcTransportConfigBuilder_v0_3.java
@@ -0,0 +1,32 @@
+package org.a2aproject.sdk.compat03.client.transport.grpc;
+
+import org.a2aproject.sdk.compat03.client.transport.spi.ClientTransportConfigBuilder_v0_3;
+import org.a2aproject.sdk.util.Assert;
+import io.grpc.Channel;
+
+import java.util.function.Function;
+
+import org.jspecify.annotations.Nullable;
+
+public class GrpcTransportConfigBuilder_v0_3 extends ClientTransportConfigBuilder_v0_3 {
+
+ private @Nullable Function channelFactory;
+
+ public GrpcTransportConfigBuilder_v0_3 channelFactory(Function channelFactory) {
+ Assert.checkNotNullParam("channelFactory", channelFactory);
+
+ this.channelFactory = channelFactory;
+
+ return this;
+ }
+
+ @Override
+ public GrpcTransportConfig_v0_3 build() {
+ if (channelFactory == null) {
+ throw new IllegalStateException("channelFactory must be set");
+ }
+ GrpcTransportConfig_v0_3 config = new GrpcTransportConfig_v0_3(channelFactory);
+ config.setInterceptors(interceptors);
+ return config;
+ }
+}
\ No newline at end of file
diff --git a/compat-0.3/client/transport/grpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/grpc/GrpcTransportConfig_v0_3.java b/compat-0.3/client/transport/grpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/grpc/GrpcTransportConfig_v0_3.java
new file mode 100644
index 000000000..34122cc53
--- /dev/null
+++ b/compat-0.3/client/transport/grpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/grpc/GrpcTransportConfig_v0_3.java
@@ -0,0 +1,21 @@
+package org.a2aproject.sdk.compat03.client.transport.grpc;
+
+import org.a2aproject.sdk.compat03.client.transport.spi.ClientTransportConfig_v0_3;
+import org.a2aproject.sdk.util.Assert;
+import io.grpc.Channel;
+
+import java.util.function.Function;
+
+public class GrpcTransportConfig_v0_3 extends ClientTransportConfig_v0_3 {
+
+ private final Function channelFactory;
+
+ public GrpcTransportConfig_v0_3(Function channelFactory) {
+ Assert.checkNotNullParam("channelFactory", channelFactory);
+ this.channelFactory = channelFactory;
+ }
+
+ public Function getChannelFactory() {
+ return this.channelFactory;
+ }
+}
\ No newline at end of file
diff --git a/compat-0.3/client/transport/grpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/grpc/GrpcTransportProvider_v0_3.java b/compat-0.3/client/transport/grpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/grpc/GrpcTransportProvider_v0_3.java
new file mode 100644
index 000000000..1faac044a
--- /dev/null
+++ b/compat-0.3/client/transport/grpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/grpc/GrpcTransportProvider_v0_3.java
@@ -0,0 +1,35 @@
+package org.a2aproject.sdk.compat03.client.transport.grpc;
+
+import org.a2aproject.sdk.compat03.client.transport.spi.ClientTransportProvider_v0_3;
+import org.a2aproject.sdk.compat03.spec.A2AClientException_v0_3;
+import org.a2aproject.sdk.compat03.spec.AgentCard_v0_3;
+import org.a2aproject.sdk.compat03.spec.TransportProtocol_v0_3;
+import io.grpc.Channel;
+
+/**
+ * Provider for gRPC transport implementation.
+ */
+public class GrpcTransportProvider_v0_3 implements ClientTransportProvider_v0_3 {
+
+ @Override
+ public GrpcTransport_v0_3 create(GrpcTransportConfig_v0_3 grpcTransportConfig, AgentCard_v0_3 agentCard, String agentUrl) throws A2AClientException_v0_3 {
+ // not making use of the interceptors for gRPC for now
+
+ Channel channel = grpcTransportConfig.getChannelFactory().apply(agentUrl);
+ if (channel != null) {
+ return new GrpcTransport_v0_3(channel, agentCard, grpcTransportConfig.getInterceptors());
+ }
+
+ throw new A2AClientException_v0_3("Missing required GrpcTransportConfig");
+ }
+
+ @Override
+ public String getTransportProtocol() {
+ return TransportProtocol_v0_3.GRPC.asString();
+ }
+
+ @Override
+ public Class getTransportProtocolClass() {
+ return GrpcTransport_v0_3.class;
+ }
+}
diff --git a/compat-0.3/client/transport/grpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/grpc/GrpcTransport_v0_3.java b/compat-0.3/client/transport/grpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/grpc/GrpcTransport_v0_3.java
new file mode 100644
index 000000000..50f73e641
--- /dev/null
+++ b/compat-0.3/client/transport/grpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/grpc/GrpcTransport_v0_3.java
@@ -0,0 +1,389 @@
+package org.a2aproject.sdk.compat03.client.transport.grpc;
+
+import static org.a2aproject.sdk.util.Assert.checkNotNullParam;
+
+import java.util.List;
+import java.util.Map;
+import java.util.function.Consumer;
+import java.util.stream.Collectors;
+
+import org.a2aproject.sdk.compat03.client.transport.spi.interceptors.ClientCallContext_v0_3;
+import org.a2aproject.sdk.compat03.client.transport.spi.ClientTransport_v0_3;
+import org.a2aproject.sdk.compat03.client.transport.spi.interceptors.ClientCallInterceptor_v0_3;
+import org.a2aproject.sdk.compat03.client.transport.spi.interceptors.PayloadAndHeaders_v0_3;
+import org.a2aproject.sdk.compat03.client.transport.spi.interceptors.auth.AuthInterceptor_v0_3;
+import org.a2aproject.sdk.compat03.grpc.A2AServiceGrpc;
+import org.a2aproject.sdk.compat03.grpc.A2AServiceGrpc.A2AServiceBlockingV2Stub;
+import org.a2aproject.sdk.compat03.grpc.A2AServiceGrpc.A2AServiceStub;
+import org.a2aproject.sdk.compat03.grpc.CancelTaskRequest;
+import org.a2aproject.sdk.compat03.grpc.CreateTaskPushNotificationConfigRequest;
+import org.a2aproject.sdk.compat03.grpc.DeleteTaskPushNotificationConfigRequest;
+import org.a2aproject.sdk.compat03.grpc.GetTaskPushNotificationConfigRequest;
+import org.a2aproject.sdk.compat03.grpc.GetTaskRequest;
+import org.a2aproject.sdk.compat03.grpc.ListTaskPushNotificationConfigRequest;
+import org.a2aproject.sdk.compat03.grpc.SendMessageRequest;
+import org.a2aproject.sdk.compat03.grpc.SendMessageResponse;
+import org.a2aproject.sdk.compat03.grpc.StreamResponse;
+import org.a2aproject.sdk.compat03.grpc.TaskSubscriptionRequest;
+import org.a2aproject.sdk.compat03.grpc.utils.ProtoUtils_v0_3.FromProto;
+import org.a2aproject.sdk.compat03.grpc.utils.ProtoUtils_v0_3.ToProto;
+import io.grpc.StatusException;
+import org.a2aproject.sdk.compat03.spec.A2AClientException_v0_3;
+import org.a2aproject.sdk.compat03.spec.AgentCard_v0_3;
+import org.a2aproject.sdk.compat03.spec.CancelTaskRequest_v0_3;
+import org.a2aproject.sdk.compat03.spec.DeleteTaskPushNotificationConfigParams_v0_3;
+import org.a2aproject.sdk.compat03.spec.DeleteTaskPushNotificationConfigRequest_v0_3;
+import org.a2aproject.sdk.compat03.spec.EventKind_v0_3;
+import org.a2aproject.sdk.compat03.spec.GetTaskPushNotificationConfigParams_v0_3;
+import org.a2aproject.sdk.compat03.spec.GetTaskPushNotificationConfigRequest_v0_3;
+import org.a2aproject.sdk.compat03.spec.GetTaskRequest_v0_3;
+import org.a2aproject.sdk.compat03.spec.ListTaskPushNotificationConfigParams_v0_3;
+import org.a2aproject.sdk.compat03.spec.ListTaskPushNotificationConfigRequest_v0_3;
+import org.a2aproject.sdk.compat03.spec.MessageSendParams_v0_3;
+import org.a2aproject.sdk.compat03.spec.SendMessageRequest_v0_3;
+import org.a2aproject.sdk.compat03.spec.SendStreamingMessageRequest_v0_3;
+import org.a2aproject.sdk.compat03.spec.SetTaskPushNotificationConfigRequest_v0_3;
+import org.a2aproject.sdk.compat03.spec.StreamingEventKind_v0_3;
+import org.a2aproject.sdk.compat03.spec.Task_v0_3;
+import org.a2aproject.sdk.compat03.spec.TaskIdParams_v0_3;
+import org.a2aproject.sdk.compat03.spec.TaskPushNotificationConfig_v0_3;
+import org.a2aproject.sdk.compat03.spec.TaskQueryParams_v0_3;
+import org.a2aproject.sdk.compat03.spec.TaskResubscriptionRequest_v0_3;
+import io.grpc.Channel;
+import io.grpc.Metadata;
+import io.grpc.StatusRuntimeException;
+import io.grpc.stub.MetadataUtils;
+import io.grpc.stub.StreamObserver;
+import org.jspecify.annotations.Nullable;
+
+public class GrpcTransport_v0_3 implements ClientTransport_v0_3 {
+
+ private static final Metadata.Key AUTHORIZATION_METADATA_KEY = Metadata.Key.of(
+ AuthInterceptor_v0_3.AUTHORIZATION,
+ Metadata.ASCII_STRING_MARSHALLER);
+ private static final Metadata.Key EXTENSIONS_KEY = Metadata.Key.of(
+ "X-A2A-Extensions",
+ Metadata.ASCII_STRING_MARSHALLER);
+ private final A2AServiceBlockingV2Stub blockingStub;
+ private final A2AServiceStub asyncStub;
+ private final @Nullable List interceptors;
+ private AgentCard_v0_3 agentCard;
+
+ public GrpcTransport_v0_3(Channel channel, AgentCard_v0_3 agentCard) {
+ this(channel, agentCard, null);
+ }
+
+ public GrpcTransport_v0_3(Channel channel, AgentCard_v0_3 agentCard, @Nullable List interceptors) {
+ checkNotNullParam("channel", channel);
+ checkNotNullParam("agentCard", agentCard);
+ this.asyncStub = A2AServiceGrpc.newStub(channel);
+ this.blockingStub = A2AServiceGrpc.newBlockingV2Stub(channel);
+ this.agentCard = agentCard;
+ this.interceptors = interceptors;
+ }
+
+ @Override
+ public EventKind_v0_3 sendMessage(MessageSendParams_v0_3 request, @Nullable ClientCallContext_v0_3 context) throws A2AClientException_v0_3 {
+ checkNotNullParam("request", request);
+
+ SendMessageRequest sendMessageRequest = createGrpcSendMessageRequest(request, context);
+ PayloadAndHeaders_v0_3 payloadAndHeaders = applyInterceptors(SendMessageRequest_v0_3.METHOD, sendMessageRequest,
+ agentCard, context);
+
+ try {
+ A2AServiceBlockingV2Stub stubWithMetadata = createBlockingStubWithMetadata(context, payloadAndHeaders);
+ SendMessageResponse response = stubWithMetadata.sendMessage(sendMessageRequest);
+ if (response.hasMsg()) {
+ return FromProto.message(response.getMsg());
+ } else if (response.hasTask()) {
+ return FromProto.task(response.getTask());
+ } else {
+ throw new A2AClientException_v0_3("Server response did not contain a message or task");
+ }
+ } catch (StatusRuntimeException | StatusException e) {
+ throw GrpcErrorMapper_v0_3.mapGrpcError(e, "Failed to send message: ");
+ }
+ }
+
+ @Override
+ public void sendMessageStreaming(MessageSendParams_v0_3 request, Consumer eventConsumer,
+ Consumer errorConsumer, @Nullable ClientCallContext_v0_3 context) throws A2AClientException_v0_3 {
+ checkNotNullParam("request", request);
+ checkNotNullParam("eventConsumer", eventConsumer);
+ SendMessageRequest grpcRequest = createGrpcSendMessageRequest(request, context);
+ PayloadAndHeaders_v0_3 payloadAndHeaders = applyInterceptors(SendStreamingMessageRequest_v0_3.METHOD,
+ grpcRequest, agentCard, context);
+ StreamObserver streamObserver = new EventStreamObserver_v0_3(eventConsumer, errorConsumer);
+
+ try {
+ A2AServiceStub stubWithMetadata = createAsyncStubWithMetadata(context, payloadAndHeaders);
+ stubWithMetadata.sendStreamingMessage(grpcRequest, streamObserver);
+ } catch (StatusRuntimeException e) {
+ throw GrpcErrorMapper_v0_3.mapGrpcError(e, "Failed to send streaming message request: ");
+ }
+ }
+
+ @Override
+ public Task_v0_3 getTask(TaskQueryParams_v0_3 request, @Nullable ClientCallContext_v0_3 context) throws A2AClientException_v0_3 {
+ checkNotNullParam("request", request);
+
+ GetTaskRequest.Builder requestBuilder = GetTaskRequest.newBuilder();
+ requestBuilder.setName("tasks/" + request.id());
+ requestBuilder.setHistoryLength(request.historyLength());
+ GetTaskRequest getTaskRequest = requestBuilder.build();
+ PayloadAndHeaders_v0_3 payloadAndHeaders = applyInterceptors(GetTaskRequest_v0_3.METHOD, getTaskRequest,
+ agentCard, context);
+
+ try {
+ A2AServiceBlockingV2Stub stubWithMetadata = createBlockingStubWithMetadata(context, payloadAndHeaders);
+ return FromProto.task(stubWithMetadata.getTask(getTaskRequest));
+ } catch (StatusRuntimeException | StatusException e) {
+ throw GrpcErrorMapper_v0_3.mapGrpcError(e, "Failed to get task: ");
+ }
+ }
+
+ @Override
+ public Task_v0_3 cancelTask(TaskIdParams_v0_3 request, @Nullable ClientCallContext_v0_3 context) throws A2AClientException_v0_3 {
+ checkNotNullParam("request", request);
+
+ CancelTaskRequest cancelTaskRequest = CancelTaskRequest.newBuilder()
+ .setName("tasks/" + request.id())
+ .build();
+ PayloadAndHeaders_v0_3 payloadAndHeaders = applyInterceptors(CancelTaskRequest_v0_3.METHOD, cancelTaskRequest,
+ agentCard, context);
+
+ try {
+ A2AServiceBlockingV2Stub stubWithMetadata = createBlockingStubWithMetadata(context, payloadAndHeaders);
+ return FromProto.task(stubWithMetadata.cancelTask(cancelTaskRequest));
+ } catch (StatusRuntimeException | StatusException e) {
+ throw GrpcErrorMapper_v0_3.mapGrpcError(e, "Failed to cancel task: ");
+ }
+ }
+
+ @Override
+ public TaskPushNotificationConfig_v0_3 setTaskPushNotificationConfiguration(TaskPushNotificationConfig_v0_3 request,
+ @Nullable ClientCallContext_v0_3 context) throws A2AClientException_v0_3 {
+ checkNotNullParam("request", request);
+
+ String configId = request.pushNotificationConfig().id();
+ CreateTaskPushNotificationConfigRequest grpcRequest = CreateTaskPushNotificationConfigRequest.newBuilder()
+ .setParent("tasks/" + request.taskId())
+ .setConfig(ToProto.taskPushNotificationConfig(request))
+ .setConfigId(configId != null ? configId : request.taskId())
+ .build();
+ PayloadAndHeaders_v0_3 payloadAndHeaders = applyInterceptors(SetTaskPushNotificationConfigRequest_v0_3.METHOD,
+ grpcRequest, agentCard, context);
+
+ try {
+ A2AServiceBlockingV2Stub stubWithMetadata = createBlockingStubWithMetadata(context, payloadAndHeaders);
+ return FromProto.taskPushNotificationConfig(stubWithMetadata.createTaskPushNotificationConfig(grpcRequest));
+ } catch (StatusRuntimeException | StatusException e) {
+ throw GrpcErrorMapper_v0_3.mapGrpcError(e, "Failed to create task push notification config: ");
+ }
+ }
+
+ @Override
+ public TaskPushNotificationConfig_v0_3 getTaskPushNotificationConfiguration(
+ GetTaskPushNotificationConfigParams_v0_3 request,
+ @Nullable ClientCallContext_v0_3 context) throws A2AClientException_v0_3 {
+ checkNotNullParam("request", request);
+
+ GetTaskPushNotificationConfigRequest grpcRequest = GetTaskPushNotificationConfigRequest.newBuilder()
+ .setName(getTaskPushNotificationConfigName(request))
+ .build();
+ PayloadAndHeaders_v0_3 payloadAndHeaders = applyInterceptors(GetTaskPushNotificationConfigRequest_v0_3.METHOD,
+ grpcRequest, agentCard, context);
+
+ try {
+ A2AServiceBlockingV2Stub stubWithMetadata = createBlockingStubWithMetadata(context, payloadAndHeaders);
+ return FromProto.taskPushNotificationConfig(stubWithMetadata.getTaskPushNotificationConfig(grpcRequest));
+ } catch (StatusRuntimeException | StatusException e) {
+ throw GrpcErrorMapper_v0_3.mapGrpcError(e, "Failed to get task push notification config: ");
+ }
+ }
+
+ @Override
+ public List listTaskPushNotificationConfigurations(
+ ListTaskPushNotificationConfigParams_v0_3 request,
+ @Nullable ClientCallContext_v0_3 context) throws A2AClientException_v0_3 {
+ checkNotNullParam("request", request);
+
+ ListTaskPushNotificationConfigRequest grpcRequest = ListTaskPushNotificationConfigRequest.newBuilder()
+ .setParent("tasks/" + request.id())
+ .build();
+ PayloadAndHeaders_v0_3 payloadAndHeaders = applyInterceptors(ListTaskPushNotificationConfigRequest_v0_3.METHOD,
+ grpcRequest, agentCard, context);
+
+ try {
+ A2AServiceBlockingV2Stub stubWithMetadata = createBlockingStubWithMetadata(context, payloadAndHeaders);
+ return stubWithMetadata.listTaskPushNotificationConfig(grpcRequest).getConfigsList().stream()
+ .map(FromProto::taskPushNotificationConfig)
+ .collect(Collectors.toList());
+ } catch (StatusRuntimeException | StatusException e) {
+ throw GrpcErrorMapper_v0_3.mapGrpcError(e, "Failed to list task push notification config: ");
+ }
+ }
+
+ @Override
+ public void deleteTaskPushNotificationConfigurations(DeleteTaskPushNotificationConfigParams_v0_3 request,
+ @Nullable ClientCallContext_v0_3 context) throws A2AClientException_v0_3 {
+ checkNotNullParam("request", request);
+
+ DeleteTaskPushNotificationConfigRequest grpcRequest = DeleteTaskPushNotificationConfigRequest.newBuilder()
+ .setName(getTaskPushNotificationConfigName(request.id(), request.pushNotificationConfigId()))
+ .build();
+ PayloadAndHeaders_v0_3 payloadAndHeaders = applyInterceptors(DeleteTaskPushNotificationConfigRequest_v0_3.METHOD,
+ grpcRequest, agentCard, context);
+
+ try {
+ A2AServiceBlockingV2Stub stubWithMetadata = createBlockingStubWithMetadata(context, payloadAndHeaders);
+ stubWithMetadata.deleteTaskPushNotificationConfig(grpcRequest);
+ } catch (StatusRuntimeException | StatusException e) {
+ throw GrpcErrorMapper_v0_3.mapGrpcError(e, "Failed to delete task push notification config: ");
+ }
+ }
+
+ @Override
+ public void resubscribe(TaskIdParams_v0_3 request, Consumer eventConsumer,
+ Consumer errorConsumer, @Nullable ClientCallContext_v0_3 context) throws A2AClientException_v0_3 {
+ checkNotNullParam("request", request);
+ checkNotNullParam("eventConsumer", eventConsumer);
+
+ TaskSubscriptionRequest grpcRequest = TaskSubscriptionRequest.newBuilder()
+ .setName("tasks/" + request.id())
+ .build();
+ PayloadAndHeaders_v0_3 payloadAndHeaders = applyInterceptors(TaskResubscriptionRequest_v0_3.METHOD,
+ grpcRequest, agentCard, context);
+
+ StreamObserver streamObserver = new EventStreamObserver_v0_3(eventConsumer, errorConsumer);
+
+ try {
+ A2AServiceStub stubWithMetadata = createAsyncStubWithMetadata(context, payloadAndHeaders);
+ stubWithMetadata.taskSubscription(grpcRequest, streamObserver);
+ } catch (StatusRuntimeException e) {
+ throw GrpcErrorMapper_v0_3.mapGrpcError(e, "Failed to resubscribe task push notification config: ");
+ }
+ }
+
+ @Override
+ public AgentCard_v0_3 getAgentCard(@Nullable ClientCallContext_v0_3 context) throws A2AClientException_v0_3 {
+ // TODO: Determine how to handle retrieving the authenticated extended agent card
+ return agentCard;
+ }
+
+ @Override
+ public void close() {
+ }
+
+ private SendMessageRequest createGrpcSendMessageRequest(MessageSendParams_v0_3 messageSendParams, @Nullable ClientCallContext_v0_3 context) {
+ SendMessageRequest.Builder builder = SendMessageRequest.newBuilder();
+ builder.setRequest(ToProto.message(messageSendParams.message()));
+ if (messageSendParams.configuration() != null) {
+ builder.setConfiguration(ToProto.messageSendConfiguration(messageSendParams.configuration()));
+ }
+ if (messageSendParams.metadata() != null) {
+ builder.setMetadata(ToProto.struct(messageSendParams.metadata()));
+ }
+ return builder.build();
+ }
+
+ /**
+ * Creates gRPC metadata from ClientCallContext headers.
+ * Extracts headers like X-A2A-Extensions and sets them as gRPC metadata.
+ * @param context the client call context containing headers, may be null
+ * @param payloadAndHeaders the payload and headers wrapper, may be null
+ * @return the gRPC metadata
+ */
+ private Metadata createGrpcMetadata(@Nullable ClientCallContext_v0_3 context, @Nullable PayloadAndHeaders_v0_3 payloadAndHeaders) {
+ Metadata metadata = new Metadata();
+
+ if (context != null && context.getHeaders() != null) {
+ // Set X-A2A-Extensions header if present
+ String extensionsHeader = context.getHeaders().get("X-A2A-Extensions");
+ if (extensionsHeader != null) {
+ metadata.put(EXTENSIONS_KEY, extensionsHeader);
+ }
+
+ // Add other headers as needed in the future
+ // For now, we only handle X-A2A-Extensions
+ }
+ if (payloadAndHeaders != null && payloadAndHeaders.getHeaders() != null) {
+ // Handle all headers from interceptors (including auth headers)
+ for (Map.Entry headerEntry : payloadAndHeaders.getHeaders().entrySet()) {
+ String headerName = headerEntry.getKey();
+ String headerValue = headerEntry.getValue();
+
+ if (headerValue != null) {
+ // Use static key for common Authorization header, create dynamic keys for others
+ if (AuthInterceptor_v0_3.AUTHORIZATION.equals(headerName)) {
+ metadata.put(AUTHORIZATION_METADATA_KEY, headerValue);
+ } else {
+ // Create a metadata key dynamically for API keys and other custom headers
+ Metadata.Key metadataKey = Metadata.Key.of(headerName, Metadata.ASCII_STRING_MARSHALLER);
+ metadata.put(metadataKey, headerValue);
+ }
+ }
+ }
+ }
+
+ return metadata;
+ }
+
+ /**
+ * Creates a blocking stub with metadata attached from the ClientCallContext.
+ *
+ * @param context the client call context
+ * @param payloadAndHeaders the payloadAndHeaders after applying any interceptors
+ * @return blocking stub with metadata interceptor
+ */
+ private A2AServiceBlockingV2Stub createBlockingStubWithMetadata(@Nullable ClientCallContext_v0_3 context,
+ PayloadAndHeaders_v0_3 payloadAndHeaders) {
+ Metadata metadata = createGrpcMetadata(context, payloadAndHeaders);
+ return blockingStub.withInterceptors(MetadataUtils.newAttachHeadersInterceptor(metadata));
+ }
+
+ /**
+ * Creates an async stub with metadata attached from the ClientCallContext.
+ *
+ * @param context the client call context
+ * @param payloadAndHeaders the payloadAndHeaders after applying any interceptors
+ * @return async stub with metadata interceptor
+ */
+ private A2AServiceStub createAsyncStubWithMetadata(@Nullable ClientCallContext_v0_3 context,
+ PayloadAndHeaders_v0_3 payloadAndHeaders) {
+ Metadata metadata = createGrpcMetadata(context, payloadAndHeaders);
+ return asyncStub.withInterceptors(MetadataUtils.newAttachHeadersInterceptor(metadata));
+ }
+
+ private String getTaskPushNotificationConfigName(GetTaskPushNotificationConfigParams_v0_3 params) {
+ return getTaskPushNotificationConfigName(params.id(), params.pushNotificationConfigId());
+ }
+
+ private String getTaskPushNotificationConfigName(String taskId, @Nullable String pushNotificationConfigId) {
+ StringBuilder name = new StringBuilder();
+ name.append("tasks/");
+ name.append(taskId);
+ if (pushNotificationConfigId != null) {
+ name.append("/pushNotificationConfigs/");
+ name.append(pushNotificationConfigId);
+ }
+ //name.append("/pushNotificationConfigs/");
+ // Use taskId as default config ID if none provided
+ //name.append(pushNotificationConfigId != null ? pushNotificationConfigId : taskId);
+ return name.toString();
+ }
+
+ private PayloadAndHeaders_v0_3 applyInterceptors(String methodName, Object payload,
+ AgentCard_v0_3 agentCard, @Nullable ClientCallContext_v0_3 clientCallContext) {
+ PayloadAndHeaders_v0_3 payloadAndHeaders = new PayloadAndHeaders_v0_3(payload,
+ clientCallContext != null ? clientCallContext.getHeaders() : null);
+ if (interceptors != null && ! interceptors.isEmpty()) {
+ for (ClientCallInterceptor_v0_3 interceptor : interceptors) {
+ payloadAndHeaders = interceptor.intercept(methodName, payloadAndHeaders.getPayload(),
+ payloadAndHeaders.getHeaders(), agentCard, clientCallContext);
+ }
+ }
+ return payloadAndHeaders;
+ }
+
+}
\ No newline at end of file
diff --git a/compat-0.3/client/transport/grpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/grpc/package-info.java b/compat-0.3/client/transport/grpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/grpc/package-info.java
new file mode 100644
index 000000000..71bb5c883
--- /dev/null
+++ b/compat-0.3/client/transport/grpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/grpc/package-info.java
@@ -0,0 +1,4 @@
+@NullMarked
+package org.a2aproject.sdk.compat03.client.transport.grpc;
+
+import org.jspecify.annotations.NullMarked;
\ No newline at end of file
diff --git a/compat-0.3/client/transport/grpc/src/main/resources/META-INF/services/org.a2aproject.sdk.compat03.client.transport.spi.ClientTransportProvider_v0_3 b/compat-0.3/client/transport/grpc/src/main/resources/META-INF/services/org.a2aproject.sdk.compat03.client.transport.spi.ClientTransportProvider_v0_3
new file mode 100644
index 000000000..613914e98
--- /dev/null
+++ b/compat-0.3/client/transport/grpc/src/main/resources/META-INF/services/org.a2aproject.sdk.compat03.client.transport.spi.ClientTransportProvider_v0_3
@@ -0,0 +1 @@
+org.a2aproject.sdk.compat03.client.transport.grpc.GrpcTransportProvider_v0_3
\ No newline at end of file
diff --git a/compat-0.3/client/transport/grpc/src/test/java/org/a2aproject/sdk/compat03/client/transport/grpc/GrpcErrorMapper_v0_3_Test.java b/compat-0.3/client/transport/grpc/src/test/java/org/a2aproject/sdk/compat03/client/transport/grpc/GrpcErrorMapper_v0_3_Test.java
new file mode 100644
index 000000000..b2e402f03
--- /dev/null
+++ b/compat-0.3/client/transport/grpc/src/test/java/org/a2aproject/sdk/compat03/client/transport/grpc/GrpcErrorMapper_v0_3_Test.java
@@ -0,0 +1,293 @@
+package org.a2aproject.sdk.compat03.client.transport.grpc;
+
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.a2aproject.sdk.compat03.spec.A2AClientException_v0_3;
+import org.a2aproject.sdk.compat03.spec.ContentTypeNotSupportedError_v0_3;
+import org.a2aproject.sdk.compat03.spec.InternalError_v0_3;
+import org.a2aproject.sdk.compat03.spec.InvalidAgentResponseError_v0_3;
+import org.a2aproject.sdk.compat03.spec.InvalidParamsError_v0_3;
+import org.a2aproject.sdk.compat03.spec.InvalidRequestError_v0_3;
+import org.a2aproject.sdk.compat03.spec.JSONParseError_v0_3;
+import org.a2aproject.sdk.compat03.spec.MethodNotFoundError_v0_3;
+import org.a2aproject.sdk.compat03.spec.PushNotificationNotSupportedError_v0_3;
+import org.a2aproject.sdk.compat03.spec.TaskNotCancelableError_v0_3;
+import org.a2aproject.sdk.compat03.spec.TaskNotFoundError_v0_3;
+import org.a2aproject.sdk.compat03.spec.UnsupportedOperationError_v0_3;
+import io.grpc.Status;
+import io.grpc.StatusRuntimeException;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests for GrpcErrorMapper - verifies correct mapping of gRPC StatusRuntimeException
+ * to v0.3 A2A error types based on description string matching and status codes.
+ */
+public class GrpcErrorMapper_v0_3_Test {
+
+ @Test
+ public void testTaskNotFoundErrorByDescription() {
+ String errorMessage = "TaskNotFoundError: Task task-123 not found";
+ StatusRuntimeException grpcException = Status.NOT_FOUND
+ .withDescription(errorMessage)
+ .asRuntimeException();
+
+ A2AClientException_v0_3 result = GrpcErrorMapper_v0_3.mapGrpcError(grpcException);
+
+ assertNotNull(result);
+ assertNotNull(result.getCause());
+ assertInstanceOf(TaskNotFoundError_v0_3.class, result.getCause());
+ assertTrue(result.getMessage().contains(errorMessage));
+ }
+
+ @Test
+ public void testTaskNotFoundErrorByStatusCode() {
+ // Test fallback to status code mapping when description doesn't contain error type
+ StatusRuntimeException grpcException = Status.NOT_FOUND
+ .withDescription("Generic not found error")
+ .asRuntimeException();
+
+ A2AClientException_v0_3 result = GrpcErrorMapper_v0_3.mapGrpcError(grpcException);
+
+ assertNotNull(result);
+ assertNotNull(result.getCause());
+ assertInstanceOf(TaskNotFoundError_v0_3.class, result.getCause());
+ }
+
+ @Test
+ public void testUnsupportedOperationErrorByDescription() {
+ String errorMessage = "UnsupportedOperationError: Operation not supported";
+ StatusRuntimeException grpcException = Status.UNIMPLEMENTED
+ .withDescription(errorMessage)
+ .asRuntimeException();
+
+ A2AClientException_v0_3 result = GrpcErrorMapper_v0_3.mapGrpcError(grpcException);
+
+ assertNotNull(result);
+ assertNotNull(result.getCause());
+ assertInstanceOf(UnsupportedOperationError_v0_3.class, result.getCause());
+ }
+
+ @Test
+ public void testUnsupportedOperationErrorByStatusCode() {
+ StatusRuntimeException grpcException = Status.UNIMPLEMENTED
+ .withDescription("Generic unimplemented error")
+ .asRuntimeException();
+
+ A2AClientException_v0_3 result = GrpcErrorMapper_v0_3.mapGrpcError(grpcException);
+
+ assertNotNull(result);
+ assertNotNull(result.getCause());
+ assertInstanceOf(UnsupportedOperationError_v0_3.class, result.getCause());
+ }
+
+ @Test
+ public void testInvalidParamsErrorByDescription() {
+ String errorMessage = "InvalidParamsError: Invalid parameters provided";
+ StatusRuntimeException grpcException = Status.INVALID_ARGUMENT
+ .withDescription(errorMessage)
+ .asRuntimeException();
+
+ A2AClientException_v0_3 result = GrpcErrorMapper_v0_3.mapGrpcError(grpcException);
+
+ assertNotNull(result);
+ assertNotNull(result.getCause());
+ assertInstanceOf(InvalidParamsError_v0_3.class, result.getCause());
+ }
+
+ @Test
+ public void testInvalidParamsErrorByStatusCode() {
+ StatusRuntimeException grpcException = Status.INVALID_ARGUMENT
+ .withDescription("Generic invalid argument")
+ .asRuntimeException();
+
+ A2AClientException_v0_3 result = GrpcErrorMapper_v0_3.mapGrpcError(grpcException);
+
+ assertNotNull(result);
+ assertNotNull(result.getCause());
+ assertInstanceOf(InvalidParamsError_v0_3.class, result.getCause());
+ }
+
+ @Test
+ public void testInvalidRequestError() {
+ String errorMessage = "InvalidRequestError: Request is malformed";
+ StatusRuntimeException grpcException = Status.INVALID_ARGUMENT
+ .withDescription(errorMessage)
+ .asRuntimeException();
+
+ A2AClientException_v0_3 result = GrpcErrorMapper_v0_3.mapGrpcError(grpcException);
+
+ assertNotNull(result);
+ assertNotNull(result.getCause());
+ assertInstanceOf(InvalidRequestError_v0_3.class, result.getCause());
+ }
+
+ @Test
+ public void testMethodNotFoundError() {
+ String errorMessage = "MethodNotFoundError: Method does not exist";
+ StatusRuntimeException grpcException = Status.NOT_FOUND
+ .withDescription(errorMessage)
+ .asRuntimeException();
+
+ A2AClientException_v0_3 result = GrpcErrorMapper_v0_3.mapGrpcError(grpcException);
+
+ assertNotNull(result);
+ assertNotNull(result.getCause());
+ assertInstanceOf(MethodNotFoundError_v0_3.class, result.getCause());
+ }
+
+ @Test
+ public void testTaskNotCancelableError() {
+ String errorMessage = "TaskNotCancelableError: Task cannot be cancelled";
+ StatusRuntimeException grpcException = Status.UNIMPLEMENTED
+ .withDescription(errorMessage)
+ .asRuntimeException();
+
+ A2AClientException_v0_3 result = GrpcErrorMapper_v0_3.mapGrpcError(grpcException);
+
+ assertNotNull(result);
+ assertNotNull(result.getCause());
+ assertInstanceOf(TaskNotCancelableError_v0_3.class, result.getCause());
+ }
+
+ @Test
+ public void testPushNotificationNotSupportedError() {
+ String errorMessage = "PushNotificationNotSupportedError: Push notifications not supported";
+ StatusRuntimeException grpcException = Status.UNIMPLEMENTED
+ .withDescription(errorMessage)
+ .asRuntimeException();
+
+ A2AClientException_v0_3 result = GrpcErrorMapper_v0_3.mapGrpcError(grpcException);
+
+ assertNotNull(result);
+ assertNotNull(result.getCause());
+ assertInstanceOf(PushNotificationNotSupportedError_v0_3.class, result.getCause());
+ }
+
+ @Test
+ public void testJSONParseError() {
+ String errorMessage = "JSONParseError: Failed to parse JSON";
+ StatusRuntimeException grpcException = Status.INTERNAL
+ .withDescription(errorMessage)
+ .asRuntimeException();
+
+ A2AClientException_v0_3 result = GrpcErrorMapper_v0_3.mapGrpcError(grpcException);
+
+ assertNotNull(result);
+ assertNotNull(result.getCause());
+ assertInstanceOf(JSONParseError_v0_3.class, result.getCause());
+ }
+
+ @Test
+ public void testContentTypeNotSupportedError() {
+ String errorMessage = "ContentTypeNotSupportedError: Content type application/xml not supported";
+ StatusRuntimeException grpcException = Status.INVALID_ARGUMENT
+ .withDescription(errorMessage)
+ .asRuntimeException();
+
+ A2AClientException_v0_3 result = GrpcErrorMapper_v0_3.mapGrpcError(grpcException);
+
+ assertNotNull(result);
+ assertNotNull(result.getCause());
+ assertInstanceOf(ContentTypeNotSupportedError_v0_3.class, result.getCause());
+
+ ContentTypeNotSupportedError_v0_3 contentTypeError = (ContentTypeNotSupportedError_v0_3) result.getCause();
+ assertNotNull(contentTypeError.getMessage());
+ assertTrue(contentTypeError.getMessage().contains("Content type application/xml not supported"));
+ }
+
+ @Test
+ public void testInvalidAgentResponseError() {
+ String errorMessage = "InvalidAgentResponseError: Agent response is invalid";
+ StatusRuntimeException grpcException = Status.INTERNAL
+ .withDescription(errorMessage)
+ .asRuntimeException();
+
+ A2AClientException_v0_3 result = GrpcErrorMapper_v0_3.mapGrpcError(grpcException);
+
+ assertNotNull(result);
+ assertNotNull(result.getCause());
+ assertInstanceOf(InvalidAgentResponseError_v0_3.class, result.getCause());
+
+ InvalidAgentResponseError_v0_3 agentResponseError = (InvalidAgentResponseError_v0_3) result.getCause();
+ assertNotNull(agentResponseError.getMessage());
+ assertTrue(agentResponseError.getMessage().contains("Agent response is invalid"));
+ }
+
+ @Test
+ public void testInternalErrorByStatusCode() {
+ StatusRuntimeException grpcException = Status.INTERNAL
+ .withDescription("Internal server error")
+ .asRuntimeException();
+
+ A2AClientException_v0_3 result = GrpcErrorMapper_v0_3.mapGrpcError(grpcException);
+
+ assertNotNull(result);
+ assertNotNull(result.getCause());
+ assertInstanceOf(InternalError_v0_3.class, result.getCause());
+ }
+
+ @Test
+ public void testCustomErrorPrefix() {
+ String errorMessage = "TaskNotFoundError: Task not found";
+ StatusRuntimeException grpcException = Status.NOT_FOUND
+ .withDescription(errorMessage)
+ .asRuntimeException();
+
+ String customPrefix = "Custom Error: ";
+ A2AClientException_v0_3 result = GrpcErrorMapper_v0_3.mapGrpcError(grpcException, customPrefix);
+
+ assertNotNull(result);
+ assertTrue(result.getMessage().startsWith(customPrefix));
+ assertInstanceOf(TaskNotFoundError_v0_3.class, result.getCause());
+ }
+
+ @Test
+ public void testAuthenticationFailed() {
+ StatusRuntimeException grpcException = Status.UNAUTHENTICATED
+ .withDescription("Authentication failed")
+ .asRuntimeException();
+
+ A2AClientException_v0_3 result = GrpcErrorMapper_v0_3.mapGrpcError(grpcException);
+
+ assertNotNull(result);
+ assertTrue(result.getMessage().contains("Authentication failed"));
+ }
+
+ @Test
+ public void testAuthorizationFailed() {
+ StatusRuntimeException grpcException = Status.PERMISSION_DENIED
+ .withDescription("Permission denied")
+ .asRuntimeException();
+
+ A2AClientException_v0_3 result = GrpcErrorMapper_v0_3.mapGrpcError(grpcException);
+
+ assertNotNull(result);
+ assertTrue(result.getMessage().contains("Authorization failed"));
+ }
+
+ @Test
+ public void testUnknownStatusCode() {
+ StatusRuntimeException grpcException = Status.DEADLINE_EXCEEDED
+ .withDescription("Request timeout")
+ .asRuntimeException();
+
+ A2AClientException_v0_3 result = GrpcErrorMapper_v0_3.mapGrpcError(grpcException);
+
+ assertNotNull(result);
+ assertTrue(result.getMessage().contains("Request timeout"));
+ }
+
+ @Test
+ public void testNullDescription() {
+ StatusRuntimeException grpcException = Status.NOT_FOUND
+ .asRuntimeException();
+
+ A2AClientException_v0_3 result = GrpcErrorMapper_v0_3.mapGrpcError(grpcException);
+
+ assertNotNull(result);
+ assertNotNull(result.getCause());
+ assertInstanceOf(TaskNotFoundError_v0_3.class, result.getCause());
+ }
+}
diff --git a/compat-0.3/client/transport/jsonrpc/pom.xml b/compat-0.3/client/transport/jsonrpc/pom.xml
new file mode 100644
index 000000000..fd099d2d0
--- /dev/null
+++ b/compat-0.3/client/transport/jsonrpc/pom.xml
@@ -0,0 +1,49 @@
+
+
+ 4.0.0
+
+
+ org.a2aproject.sdk
+ a2a-java-sdk-compat-0.3-parent
+ 1.0.0.Beta2-SNAPSHOT
+ ../../..
+
+ a2a-java-sdk-compat-0.3-client-transport-jsonrpc
+ jar
+
+ Java SDK A2A Compat 0.3 Client Transport: JSONRPC
+ Java SDK for the Agent2Agent Protocol (A2A) - JSONRPC Client Transport
+
+
+
+ ${project.groupId}
+ a2a-java-sdk-compat-0.3-http-client
+
+
+ ${project.groupId}
+ a2a-java-sdk-compat-0.3-client-transport-spi
+
+
+ ${project.groupId}
+ a2a-java-sdk-common
+
+
+ ${project.groupId}
+ a2a-java-sdk-compat-0.3-spec
+
+
+ org.junit.jupiter
+ junit-jupiter-api
+ test
+
+
+
+ org.mock-server
+ mockserver-netty
+ test
+
+
+
+
\ No newline at end of file
diff --git a/compat-0.3/client/transport/jsonrpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/jsonrpc/JSONRPCTransportConfigBuilder_v0_3.java b/compat-0.3/client/transport/jsonrpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/jsonrpc/JSONRPCTransportConfigBuilder_v0_3.java
new file mode 100644
index 000000000..be9d95608
--- /dev/null
+++ b/compat-0.3/client/transport/jsonrpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/jsonrpc/JSONRPCTransportConfigBuilder_v0_3.java
@@ -0,0 +1,28 @@
+package org.a2aproject.sdk.compat03.client.transport.jsonrpc;
+
+import org.a2aproject.sdk.compat03.client.http.A2AHttpClient_v0_3;
+import org.a2aproject.sdk.compat03.client.http.JdkA2AHttpClient_v0_3;
+import org.a2aproject.sdk.compat03.client.transport.spi.ClientTransportConfigBuilder_v0_3;
+
+public class JSONRPCTransportConfigBuilder_v0_3 extends ClientTransportConfigBuilder_v0_3 {
+
+ private A2AHttpClient_v0_3 httpClient;
+
+ public JSONRPCTransportConfigBuilder_v0_3 httpClient(A2AHttpClient_v0_3 httpClient) {
+ this.httpClient = httpClient;
+
+ return this;
+ }
+
+ @Override
+ public JSONRPCTransportConfig_v0_3 build() {
+ // No HTTP client provided, fallback to the default one (JDK-based implementation)
+ if (httpClient == null) {
+ httpClient = new JdkA2AHttpClient_v0_3();
+ }
+
+ JSONRPCTransportConfig_v0_3 config = new JSONRPCTransportConfig_v0_3(httpClient);
+ config.setInterceptors(this.interceptors);
+ return config;
+ }
+}
\ No newline at end of file
diff --git a/compat-0.3/client/transport/jsonrpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/jsonrpc/JSONRPCTransportConfig_v0_3.java b/compat-0.3/client/transport/jsonrpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/jsonrpc/JSONRPCTransportConfig_v0_3.java
new file mode 100644
index 000000000..7e01e3e56
--- /dev/null
+++ b/compat-0.3/client/transport/jsonrpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/jsonrpc/JSONRPCTransportConfig_v0_3.java
@@ -0,0 +1,21 @@
+package org.a2aproject.sdk.compat03.client.transport.jsonrpc;
+
+import org.a2aproject.sdk.compat03.client.transport.spi.ClientTransportConfig_v0_3;
+import org.a2aproject.sdk.compat03.client.http.A2AHttpClient_v0_3;
+
+public class JSONRPCTransportConfig_v0_3 extends ClientTransportConfig_v0_3 {
+
+ private final A2AHttpClient_v0_3 httpClient;
+
+ public JSONRPCTransportConfig_v0_3() {
+ this.httpClient = null;
+ }
+
+ public JSONRPCTransportConfig_v0_3(A2AHttpClient_v0_3 httpClient) {
+ this.httpClient = httpClient;
+ }
+
+ public A2AHttpClient_v0_3 getHttpClient() {
+ return httpClient;
+ }
+}
\ No newline at end of file
diff --git a/compat-0.3/client/transport/jsonrpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/jsonrpc/JSONRPCTransportProvider_v0_3.java b/compat-0.3/client/transport/jsonrpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/jsonrpc/JSONRPCTransportProvider_v0_3.java
new file mode 100644
index 000000000..afe66dadf
--- /dev/null
+++ b/compat-0.3/client/transport/jsonrpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/jsonrpc/JSONRPCTransportProvider_v0_3.java
@@ -0,0 +1,29 @@
+package org.a2aproject.sdk.compat03.client.transport.jsonrpc;
+
+import org.a2aproject.sdk.compat03.client.http.JdkA2AHttpClient_v0_3;
+import org.a2aproject.sdk.compat03.client.transport.spi.ClientTransportProvider_v0_3;
+import org.a2aproject.sdk.compat03.spec.A2AClientException_v0_3;
+import org.a2aproject.sdk.compat03.spec.AgentCard_v0_3;
+import org.a2aproject.sdk.compat03.spec.TransportProtocol_v0_3;
+
+public class JSONRPCTransportProvider_v0_3 implements ClientTransportProvider_v0_3 {
+
+ @Override
+ public JSONRPCTransport_v0_3 create(JSONRPCTransportConfig_v0_3 clientTransportConfig, AgentCard_v0_3 agentCard, String agentUrl) throws A2AClientException_v0_3 {
+ if (clientTransportConfig == null) {
+ clientTransportConfig = new JSONRPCTransportConfig_v0_3(new JdkA2AHttpClient_v0_3());
+ }
+
+ return new JSONRPCTransport_v0_3(clientTransportConfig.getHttpClient(), agentCard, agentUrl, clientTransportConfig.getInterceptors());
+ }
+
+ @Override
+ public String getTransportProtocol() {
+ return TransportProtocol_v0_3.JSONRPC.asString();
+ }
+
+ @Override
+ public Class getTransportProtocolClass() {
+ return JSONRPCTransport_v0_3.class;
+ }
+}
diff --git a/compat-0.3/client/transport/jsonrpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/jsonrpc/JSONRPCTransport_v0_3.java b/compat-0.3/client/transport/jsonrpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/jsonrpc/JSONRPCTransport_v0_3.java
new file mode 100644
index 000000000..d379277cd
--- /dev/null
+++ b/compat-0.3/client/transport/jsonrpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/jsonrpc/JSONRPCTransport_v0_3.java
@@ -0,0 +1,424 @@
+package org.a2aproject.sdk.compat03.client.transport.jsonrpc;
+
+import static org.a2aproject.sdk.util.Assert.checkNotNullParam;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Consumer;
+
+import org.a2aproject.sdk.compat03.json.JsonProcessingException_v0_3;
+import org.a2aproject.sdk.compat03.json.JsonUtil_v0_3;
+
+import org.a2aproject.sdk.compat03.client.http.A2ACardResolver_v0_3;
+import org.a2aproject.sdk.compat03.client.transport.spi.interceptors.ClientCallContext_v0_3;
+import org.a2aproject.sdk.compat03.client.transport.spi.interceptors.ClientCallInterceptor_v0_3;
+import org.a2aproject.sdk.compat03.client.transport.spi.interceptors.PayloadAndHeaders_v0_3;
+import org.a2aproject.sdk.compat03.client.http.A2AHttpClient_v0_3;
+import org.a2aproject.sdk.compat03.client.http.A2AHttpResponse_v0_3;
+import org.a2aproject.sdk.compat03.client.http.JdkA2AHttpClient_v0_3;
+import org.a2aproject.sdk.compat03.client.transport.spi.ClientTransport_v0_3;
+import org.a2aproject.sdk.compat03.spec.A2AClientError_v0_3;
+import org.a2aproject.sdk.compat03.spec.A2AClientException_v0_3;
+import org.a2aproject.sdk.compat03.spec.AgentCard_v0_3;
+import org.a2aproject.sdk.compat03.spec.CancelTaskRequest_v0_3;
+import org.a2aproject.sdk.compat03.spec.CancelTaskResponse_v0_3;
+
+import org.a2aproject.sdk.compat03.spec.DeleteTaskPushNotificationConfigParams_v0_3;
+import org.a2aproject.sdk.compat03.spec.EventKind_v0_3;
+import org.a2aproject.sdk.compat03.spec.GetAuthenticatedExtendedCardRequest_v0_3;
+import org.a2aproject.sdk.compat03.spec.GetAuthenticatedExtendedCardResponse_v0_3;
+import org.a2aproject.sdk.compat03.spec.GetTaskPushNotificationConfigParams_v0_3;
+import org.a2aproject.sdk.compat03.spec.GetTaskPushNotificationConfigRequest_v0_3;
+import org.a2aproject.sdk.compat03.spec.GetTaskPushNotificationConfigResponse_v0_3;
+import org.a2aproject.sdk.compat03.spec.GetTaskRequest_v0_3;
+import org.a2aproject.sdk.compat03.spec.GetTaskResponse_v0_3;
+import org.a2aproject.sdk.compat03.spec.JSONRPCError_v0_3;
+import org.a2aproject.sdk.compat03.spec.JSONRPCMessage_v0_3;
+import org.a2aproject.sdk.compat03.spec.JSONRPCResponse_v0_3;
+
+import org.a2aproject.sdk.compat03.spec.ListTaskPushNotificationConfigParams_v0_3;
+import org.a2aproject.sdk.compat03.spec.ListTaskPushNotificationConfigRequest_v0_3;
+import org.a2aproject.sdk.compat03.spec.ListTaskPushNotificationConfigResponse_v0_3;
+import org.a2aproject.sdk.compat03.spec.DeleteTaskPushNotificationConfigRequest_v0_3;
+import org.a2aproject.sdk.compat03.spec.DeleteTaskPushNotificationConfigResponse_v0_3;
+import org.a2aproject.sdk.compat03.spec.MessageSendParams_v0_3;
+import org.a2aproject.sdk.compat03.spec.SendMessageRequest_v0_3;
+import org.a2aproject.sdk.compat03.spec.SendMessageResponse_v0_3;
+import org.a2aproject.sdk.compat03.spec.SendStreamingMessageRequest_v0_3;
+import org.a2aproject.sdk.compat03.spec.SetTaskPushNotificationConfigRequest_v0_3;
+import org.a2aproject.sdk.compat03.spec.SetTaskPushNotificationConfigResponse_v0_3;
+import org.a2aproject.sdk.compat03.spec.StreamingEventKind_v0_3;
+import org.a2aproject.sdk.compat03.spec.Task_v0_3;
+import org.a2aproject.sdk.compat03.spec.TaskIdParams_v0_3;
+import org.a2aproject.sdk.compat03.spec.TaskPushNotificationConfig_v0_3;
+import org.a2aproject.sdk.compat03.spec.TaskQueryParams_v0_3;
+import org.a2aproject.sdk.compat03.spec.TaskResubscriptionRequest_v0_3;
+import org.a2aproject.sdk.compat03.client.transport.jsonrpc.sse.SSEEventListener_v0_3;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.atomic.AtomicReference;
+
+public class JSONRPCTransport_v0_3 implements ClientTransport_v0_3 {
+
+ private static final Class SEND_MESSAGE_RESPONSE_REFERENCE = SendMessageResponse_v0_3.class;
+ private static final Class GET_TASK_RESPONSE_REFERENCE = GetTaskResponse_v0_3.class;
+ private static final Class CANCEL_TASK_RESPONSE_REFERENCE = CancelTaskResponse_v0_3.class;
+ private static final Class GET_TASK_PUSH_NOTIFICATION_CONFIG_RESPONSE_REFERENCE = GetTaskPushNotificationConfigResponse_v0_3.class;
+ private static final Class SET_TASK_PUSH_NOTIFICATION_CONFIG_RESPONSE_REFERENCE = SetTaskPushNotificationConfigResponse_v0_3.class;
+ private static final Class LIST_TASK_PUSH_NOTIFICATION_CONFIG_RESPONSE_REFERENCE = ListTaskPushNotificationConfigResponse_v0_3.class;
+ private static final Class DELETE_TASK_PUSH_NOTIFICATION_CONFIG_RESPONSE_REFERENCE = DeleteTaskPushNotificationConfigResponse_v0_3.class;
+ private static final Class GET_AUTHENTICATED_EXTENDED_CARD_RESPONSE_REFERENCE = GetAuthenticatedExtendedCardResponse_v0_3.class;
+
+ private final A2AHttpClient_v0_3 httpClient;
+ private final String agentUrl;
+ private final List interceptors;
+ private AgentCard_v0_3 agentCard;
+ private boolean needsExtendedCard = false;
+
+ public JSONRPCTransport_v0_3(String agentUrl) {
+ this(null, null, agentUrl, null);
+ }
+
+ public JSONRPCTransport_v0_3(AgentCard_v0_3 agentCard) {
+ this(null, agentCard, agentCard.url(), null);
+ }
+
+ public JSONRPCTransport_v0_3(A2AHttpClient_v0_3 httpClient, AgentCard_v0_3 agentCard,
+ String agentUrl, List interceptors) {
+ this.httpClient = httpClient == null ? new JdkA2AHttpClient_v0_3() : httpClient;
+ this.agentCard = agentCard;
+ this.agentUrl = agentUrl;
+ this.interceptors = interceptors;
+ this.needsExtendedCard = agentCard == null || agentCard.supportsAuthenticatedExtendedCard();
+ }
+
+ @Override
+ public EventKind_v0_3 sendMessage(MessageSendParams_v0_3 request, ClientCallContext_v0_3 context) throws A2AClientException_v0_3 {
+ checkNotNullParam("request", request);
+ SendMessageRequest_v0_3 sendMessageRequest = new SendMessageRequest_v0_3.Builder()
+ .jsonrpc(JSONRPCMessage_v0_3.JSONRPC_VERSION)
+ .method(SendMessageRequest_v0_3.METHOD)
+ .params(request)
+ .build(); // id will be randomly generated
+
+ PayloadAndHeaders_v0_3 payloadAndHeaders = applyInterceptors(SendMessageRequest_v0_3.METHOD, sendMessageRequest,
+ agentCard, context);
+
+ try {
+ String httpResponseBody = sendPostRequest(payloadAndHeaders);
+ SendMessageResponse_v0_3 response = unmarshalResponse(httpResponseBody, SEND_MESSAGE_RESPONSE_REFERENCE);
+ return response.getResult();
+ } catch (A2AClientException_v0_3 e) {
+ throw e;
+ } catch (IOException | InterruptedException | JsonProcessingException_v0_3 e) {
+ throw new A2AClientException_v0_3("Failed to send message: " + e, e);
+ }
+ }
+
+ @Override
+ public void sendMessageStreaming(MessageSendParams_v0_3 request, Consumer eventConsumer,
+ Consumer errorConsumer, ClientCallContext_v0_3 context) throws A2AClientException_v0_3 {
+ checkNotNullParam("request", request);
+ checkNotNullParam("eventConsumer", eventConsumer);
+ SendStreamingMessageRequest_v0_3 sendStreamingMessageRequest = new SendStreamingMessageRequest_v0_3.Builder()
+ .jsonrpc(JSONRPCMessage_v0_3.JSONRPC_VERSION)
+ .method(SendStreamingMessageRequest_v0_3.METHOD)
+ .params(request)
+ .build(); // id will be randomly generated
+
+ PayloadAndHeaders_v0_3 payloadAndHeaders = applyInterceptors(SendStreamingMessageRequest_v0_3.METHOD,
+ sendStreamingMessageRequest, agentCard, context);
+
+ AtomicReference> ref = new AtomicReference<>();
+ SSEEventListener_v0_3 sseEventListener = new SSEEventListener_v0_3(eventConsumer, errorConsumer);
+
+ try {
+ A2AHttpClient_v0_3.PostBuilder builder = createPostBuilder(payloadAndHeaders);
+ ref.set(builder.postAsyncSSE(
+ msg -> sseEventListener.onMessage(msg, ref.get()),
+ throwable -> sseEventListener.onError(throwable, ref.get()),
+ () -> {
+ // Signal normal stream completion to error handler (null error means success)
+ sseEventListener.onComplete();
+ }));
+ } catch (IOException e) {
+ throw new A2AClientException_v0_3("Failed to send streaming message request: " + e, e);
+ } catch (InterruptedException e) {
+ throw new A2AClientException_v0_3("Send streaming message request timed out: " + e, e);
+ } catch (JsonProcessingException_v0_3 e) {
+ throw new A2AClientException_v0_3("Failed to process JSON for streaming message request: " + e, e);
+ }
+ }
+
+ @Override
+ public Task_v0_3 getTask(TaskQueryParams_v0_3 request, ClientCallContext_v0_3 context) throws A2AClientException_v0_3 {
+ checkNotNullParam("request", request);
+ GetTaskRequest_v0_3 getTaskRequest = new GetTaskRequest_v0_3.Builder()
+ .jsonrpc(JSONRPCMessage_v0_3.JSONRPC_VERSION)
+ .method(GetTaskRequest_v0_3.METHOD)
+ .params(request)
+ .build(); // id will be randomly generated
+
+ PayloadAndHeaders_v0_3 payloadAndHeaders = applyInterceptors(GetTaskRequest_v0_3.METHOD, getTaskRequest,
+ agentCard, context);
+
+ try {
+ String httpResponseBody = sendPostRequest(payloadAndHeaders);
+ GetTaskResponse_v0_3 response = unmarshalResponse(httpResponseBody, GET_TASK_RESPONSE_REFERENCE);
+ return response.getResult();
+ } catch (A2AClientException_v0_3 e) {
+ throw e;
+ } catch (IOException | InterruptedException | JsonProcessingException_v0_3 e) {
+ throw new A2AClientException_v0_3("Failed to get task: " + e, e);
+ }
+ }
+
+ @Override
+ public Task_v0_3 cancelTask(TaskIdParams_v0_3 request, ClientCallContext_v0_3 context) throws A2AClientException_v0_3 {
+ checkNotNullParam("request", request);
+ CancelTaskRequest_v0_3 cancelTaskRequest = new CancelTaskRequest_v0_3.Builder()
+ .jsonrpc(JSONRPCMessage_v0_3.JSONRPC_VERSION)
+ .method(CancelTaskRequest_v0_3.METHOD)
+ .params(request)
+ .build(); // id will be randomly generated
+
+ PayloadAndHeaders_v0_3 payloadAndHeaders = applyInterceptors(CancelTaskRequest_v0_3.METHOD, cancelTaskRequest,
+ agentCard, context);
+
+ try {
+ String httpResponseBody = sendPostRequest(payloadAndHeaders);
+ CancelTaskResponse_v0_3 response = unmarshalResponse(httpResponseBody, CANCEL_TASK_RESPONSE_REFERENCE);
+ return response.getResult();
+ } catch (A2AClientException_v0_3 e) {
+ throw e;
+ } catch (IOException | InterruptedException | JsonProcessingException_v0_3 e) {
+ throw new A2AClientException_v0_3("Failed to cancel task: " + e, e);
+ }
+ }
+
+ @Override
+ public TaskPushNotificationConfig_v0_3 setTaskPushNotificationConfiguration(TaskPushNotificationConfig_v0_3 request,
+ ClientCallContext_v0_3 context) throws A2AClientException_v0_3 {
+ checkNotNullParam("request", request);
+ SetTaskPushNotificationConfigRequest_v0_3 setTaskPushNotificationRequest = new SetTaskPushNotificationConfigRequest_v0_3.Builder()
+ .jsonrpc(JSONRPCMessage_v0_3.JSONRPC_VERSION)
+ .method(SetTaskPushNotificationConfigRequest_v0_3.METHOD)
+ .params(request)
+ .build(); // id will be randomly generated
+
+ PayloadAndHeaders_v0_3 payloadAndHeaders = applyInterceptors(SetTaskPushNotificationConfigRequest_v0_3.METHOD,
+ setTaskPushNotificationRequest, agentCard, context);
+
+ try {
+ String httpResponseBody = sendPostRequest(payloadAndHeaders);
+ SetTaskPushNotificationConfigResponse_v0_3 response = unmarshalResponse(httpResponseBody,
+ SET_TASK_PUSH_NOTIFICATION_CONFIG_RESPONSE_REFERENCE);
+ return response.getResult();
+ } catch (A2AClientException_v0_3 e) {
+ throw e;
+ } catch (IOException | InterruptedException | JsonProcessingException_v0_3 e) {
+ throw new A2AClientException_v0_3("Failed to set task push notification config: " + e, e);
+ }
+ }
+
+ @Override
+ public TaskPushNotificationConfig_v0_3 getTaskPushNotificationConfiguration(GetTaskPushNotificationConfigParams_v0_3 request,
+ ClientCallContext_v0_3 context) throws A2AClientException_v0_3 {
+ checkNotNullParam("request", request);
+ GetTaskPushNotificationConfigRequest_v0_3 getTaskPushNotificationRequest = new GetTaskPushNotificationConfigRequest_v0_3.Builder()
+ .jsonrpc(JSONRPCMessage_v0_3.JSONRPC_VERSION)
+ .method(GetTaskPushNotificationConfigRequest_v0_3.METHOD)
+ .params(request)
+ .build(); // id will be randomly generated
+
+ PayloadAndHeaders_v0_3 payloadAndHeaders = applyInterceptors(GetTaskPushNotificationConfigRequest_v0_3.METHOD,
+ getTaskPushNotificationRequest, agentCard, context);
+
+ try {
+ String httpResponseBody = sendPostRequest(payloadAndHeaders);
+ GetTaskPushNotificationConfigResponse_v0_3 response = unmarshalResponse(httpResponseBody,
+ GET_TASK_PUSH_NOTIFICATION_CONFIG_RESPONSE_REFERENCE);
+ return response.getResult();
+ } catch (A2AClientException_v0_3 e) {
+ throw e;
+ } catch (IOException | InterruptedException | JsonProcessingException_v0_3 e) {
+ throw new A2AClientException_v0_3("Failed to get task push notification config: " + e, e);
+ }
+ }
+
+ @Override
+ public List listTaskPushNotificationConfigurations(
+ ListTaskPushNotificationConfigParams_v0_3 request,
+ ClientCallContext_v0_3 context) throws A2AClientException_v0_3 {
+ checkNotNullParam("request", request);
+ ListTaskPushNotificationConfigRequest_v0_3 listTaskPushNotificationRequest = new ListTaskPushNotificationConfigRequest_v0_3.Builder()
+ .jsonrpc(JSONRPCMessage_v0_3.JSONRPC_VERSION)
+ .method(ListTaskPushNotificationConfigRequest_v0_3.METHOD)
+ .params(request)
+ .build(); // id will be randomly generated
+
+ PayloadAndHeaders_v0_3 payloadAndHeaders = applyInterceptors(ListTaskPushNotificationConfigRequest_v0_3.METHOD,
+ listTaskPushNotificationRequest, agentCard, context);
+
+ try {
+ String httpResponseBody = sendPostRequest(payloadAndHeaders);
+ ListTaskPushNotificationConfigResponse_v0_3 response = unmarshalResponse(httpResponseBody,
+ LIST_TASK_PUSH_NOTIFICATION_CONFIG_RESPONSE_REFERENCE);
+ return response.getResult();
+ } catch (A2AClientException_v0_3 e) {
+ throw e;
+ } catch (IOException | InterruptedException | JsonProcessingException_v0_3 e) {
+ throw new A2AClientException_v0_3("Failed to list task push notification configs: " + e, e);
+ }
+ }
+
+ @Override
+ public void deleteTaskPushNotificationConfigurations(DeleteTaskPushNotificationConfigParams_v0_3 request,
+ ClientCallContext_v0_3 context) throws A2AClientException_v0_3 {
+ checkNotNullParam("request", request);
+ DeleteTaskPushNotificationConfigRequest_v0_3 deleteTaskPushNotificationRequest = new DeleteTaskPushNotificationConfigRequest_v0_3.Builder()
+ .jsonrpc(JSONRPCMessage_v0_3.JSONRPC_VERSION)
+ .method(DeleteTaskPushNotificationConfigRequest_v0_3.METHOD)
+ .params(request)
+ .build(); // id will be randomly generated
+
+ PayloadAndHeaders_v0_3 payloadAndHeaders = applyInterceptors(DeleteTaskPushNotificationConfigRequest_v0_3.METHOD,
+ deleteTaskPushNotificationRequest, agentCard, context);
+
+ try {
+ String httpResponseBody = sendPostRequest(payloadAndHeaders);
+ unmarshalResponse(httpResponseBody, DELETE_TASK_PUSH_NOTIFICATION_CONFIG_RESPONSE_REFERENCE);
+ } catch (A2AClientException_v0_3 e) {
+ throw e;
+ } catch (IOException | InterruptedException | JsonProcessingException_v0_3 e) {
+ throw new A2AClientException_v0_3("Failed to delete task push notification configs: " + e, e);
+ }
+ }
+
+ @Override
+ public void resubscribe(TaskIdParams_v0_3 request, Consumer eventConsumer,
+ Consumer errorConsumer, ClientCallContext_v0_3 context) throws A2AClientException_v0_3 {
+ checkNotNullParam("request", request);
+ checkNotNullParam("eventConsumer", eventConsumer);
+ checkNotNullParam("errorConsumer", errorConsumer);
+ TaskResubscriptionRequest_v0_3 taskResubscriptionRequest = new TaskResubscriptionRequest_v0_3.Builder()
+ .jsonrpc(JSONRPCMessage_v0_3.JSONRPC_VERSION)
+ .method(TaskResubscriptionRequest_v0_3.METHOD)
+ .params(request)
+ .build(); // id will be randomly generated
+
+ PayloadAndHeaders_v0_3 payloadAndHeaders = applyInterceptors(TaskResubscriptionRequest_v0_3.METHOD,
+ taskResubscriptionRequest, agentCard, context);
+
+ AtomicReference> ref = new AtomicReference<>();
+ SSEEventListener_v0_3 sseEventListener = new SSEEventListener_v0_3(eventConsumer, errorConsumer);
+
+ try {
+ A2AHttpClient_v0_3.PostBuilder builder = createPostBuilder(payloadAndHeaders);
+ ref.set(builder.postAsyncSSE(
+ msg -> sseEventListener.onMessage(msg, ref.get()),
+ throwable -> sseEventListener.onError(throwable, ref.get()),
+ () -> {
+ // Signal normal stream completion to error handler (null error means success)
+ sseEventListener.onComplete();
+ }));
+ } catch (IOException e) {
+ throw new A2AClientException_v0_3("Failed to send task resubscription request: " + e, e);
+ } catch (InterruptedException e) {
+ throw new A2AClientException_v0_3("Task resubscription request timed out: " + e, e);
+ } catch (JsonProcessingException_v0_3 e) {
+ throw new A2AClientException_v0_3("Failed to process JSON for task resubscription request: " + e, e);
+ }
+ }
+
+ @Override
+ public AgentCard_v0_3 getAgentCard(ClientCallContext_v0_3 context) throws A2AClientException_v0_3 {
+ A2ACardResolver_v0_3 resolver;
+ try {
+ if (agentCard == null) {
+ resolver = new A2ACardResolver_v0_3(httpClient, agentUrl, null, getHttpHeaders(context));
+ agentCard = resolver.getAgentCard();
+ needsExtendedCard = agentCard.supportsAuthenticatedExtendedCard();
+ }
+ if (!needsExtendedCard) {
+ return agentCard;
+ }
+
+ GetAuthenticatedExtendedCardRequest_v0_3 getExtendedAgentCardRequest = new GetAuthenticatedExtendedCardRequest_v0_3.Builder()
+ .jsonrpc(JSONRPCMessage_v0_3.JSONRPC_VERSION)
+ .method(GetAuthenticatedExtendedCardRequest_v0_3.METHOD)
+ .build(); // id will be randomly generated
+
+ PayloadAndHeaders_v0_3 payloadAndHeaders = applyInterceptors(GetAuthenticatedExtendedCardRequest_v0_3.METHOD,
+ getExtendedAgentCardRequest, agentCard, context);
+
+ try {
+ String httpResponseBody = sendPostRequest(payloadAndHeaders);
+ GetAuthenticatedExtendedCardResponse_v0_3 response = unmarshalResponse(httpResponseBody,
+ GET_AUTHENTICATED_EXTENDED_CARD_RESPONSE_REFERENCE);
+ agentCard = response.getResult();
+ needsExtendedCard = false;
+ return agentCard;
+ } catch (IOException | InterruptedException | JsonProcessingException_v0_3 e) {
+ throw new A2AClientException_v0_3("Failed to get authenticated extended agent card: " + e, e);
+ }
+ } catch(A2AClientError_v0_3 e){
+ throw new A2AClientException_v0_3("Failed to get agent card: " + e, e);
+ }
+ }
+
+ @Override
+ public void close() {
+ // no-op
+ }
+
+ private PayloadAndHeaders_v0_3 applyInterceptors(String methodName, Object payload,
+ AgentCard_v0_3 agentCard, ClientCallContext_v0_3 clientCallContext) {
+ PayloadAndHeaders_v0_3 payloadAndHeaders = new PayloadAndHeaders_v0_3(payload, getHttpHeaders(clientCallContext));
+ if (interceptors != null && ! interceptors.isEmpty()) {
+ for (ClientCallInterceptor_v0_3 interceptor : interceptors) {
+ payloadAndHeaders = interceptor.intercept(methodName, payloadAndHeaders.getPayload(),
+ payloadAndHeaders.getHeaders(), agentCard, clientCallContext);
+ }
+ }
+ return payloadAndHeaders;
+ }
+
+ private String sendPostRequest(PayloadAndHeaders_v0_3 payloadAndHeaders) throws IOException, InterruptedException, JsonProcessingException_v0_3 {
+ A2AHttpClient_v0_3.PostBuilder builder = createPostBuilder(payloadAndHeaders);
+ A2AHttpResponse_v0_3 response = builder.post();
+ if (!response.success()) {
+ throw new IOException("Request failed " + response.status());
+ }
+ return response.body();
+ }
+
+ private A2AHttpClient_v0_3.PostBuilder createPostBuilder(PayloadAndHeaders_v0_3 payloadAndHeaders) throws JsonProcessingException_v0_3 {
+ A2AHttpClient_v0_3.PostBuilder postBuilder = httpClient.createPost()
+ .url(agentUrl)
+ .addHeader("Content-Type", "application/json")
+ .body(JsonUtil_v0_3.toJson(payloadAndHeaders.getPayload()));
+
+ if (payloadAndHeaders.getHeaders() != null) {
+ for (Map.Entry entry : payloadAndHeaders.getHeaders().entrySet()) {
+ postBuilder.addHeader(entry.getKey(), entry.getValue());
+ }
+ }
+
+ return postBuilder;
+ }
+
+ private > T unmarshalResponse(String response, Class responseClass)
+ throws A2AClientException_v0_3, JsonProcessingException_v0_3 {
+ T value = JsonUtil_v0_3.fromJson(response, responseClass);
+ JSONRPCError_v0_3 error = value.getError();
+ if (error != null) {
+ throw new A2AClientException_v0_3(error.getMessage() + (error.getData() != null ? ": " + error.getData() : ""), error);
+ }
+ return value;
+ }
+
+ private Map getHttpHeaders(ClientCallContext_v0_3 context) {
+ return context != null ? context.getHeaders() : null;
+ }
+}
\ No newline at end of file
diff --git a/compat-0.3/client/transport/jsonrpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/jsonrpc/sse/SSEEventListener_v0_3.java b/compat-0.3/client/transport/jsonrpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/jsonrpc/sse/SSEEventListener_v0_3.java
new file mode 100644
index 000000000..6ae5a944c
--- /dev/null
+++ b/compat-0.3/client/transport/jsonrpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/jsonrpc/sse/SSEEventListener_v0_3.java
@@ -0,0 +1,88 @@
+package org.a2aproject.sdk.compat03.client.transport.jsonrpc.sse;
+
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+import com.google.gson.JsonSyntaxException;
+import org.a2aproject.sdk.compat03.json.JsonProcessingException_v0_3;
+import org.a2aproject.sdk.compat03.json.JsonUtil_v0_3;
+import org.a2aproject.sdk.compat03.spec.JSONRPCError_v0_3;
+import org.a2aproject.sdk.compat03.spec.StreamingEventKind_v0_3;
+import org.a2aproject.sdk.compat03.spec.TaskStatusUpdateEvent_v0_3;
+
+import java.util.concurrent.Future;
+import java.util.function.Consumer;
+import java.util.logging.Logger;
+
+public class SSEEventListener_v0_3 {
+ private static final Logger log = Logger.getLogger(SSEEventListener_v0_3.class.getName());
+ private final Consumer eventHandler;
+ private final Consumer errorHandler;
+ private volatile boolean completed = false;
+
+ public SSEEventListener_v0_3(Consumer eventHandler,
+ Consumer errorHandler) {
+ this.eventHandler = eventHandler;
+ this.errorHandler = errorHandler;
+ }
+
+ public void onMessage(String message, Future completableFuture) {
+ try {
+ handleMessage(JsonParser.parseString(message).getAsJsonObject(), completableFuture);
+ } catch (JsonSyntaxException e) {
+ log.warning("Failed to parse JSON message: " + message);
+ } catch (JsonProcessingException_v0_3 e) {
+ log.warning("Failed to process JSON message: " + message);
+ } catch (IllegalArgumentException e) {
+ log.warning("Invalid message format: " + message);
+ if (errorHandler != null) {
+ errorHandler.accept(e);
+ }
+ completableFuture.cancel(true); // close SSE channel
+ }
+ }
+
+ public void onError(Throwable throwable, Future future) {
+ if (errorHandler != null) {
+ errorHandler.accept(throwable);
+ }
+ future.cancel(true); // close SSE channel
+ }
+
+ public void onComplete() {
+ // Idempotent: only signal completion once, even if called multiple times
+ if (completed) {
+ log.fine("SSEEventListener.onComplete() called again - ignoring (already completed)");
+ return;
+ }
+ completed = true;
+
+ // Signal normal stream completion (null error means successful completion)
+ log.fine("SSEEventListener.onComplete() called - signaling successful stream completion");
+ if (errorHandler != null) {
+ log.fine("Calling errorHandler.accept(null) to signal successful completion");
+ errorHandler.accept(null);
+ } else {
+ log.warning("errorHandler is null, cannot signal completion");
+ }
+ }
+
+ private void handleMessage(JsonObject jsonObject, Future future) throws JsonProcessingException_v0_3 {
+ if (jsonObject.has("error")) {
+ JSONRPCError_v0_3 error = JsonUtil_v0_3.fromJson(jsonObject.get("error").toString(), JSONRPCError_v0_3.class);
+ if (errorHandler != null) {
+ errorHandler.accept(error);
+ }
+ } else if (jsonObject.has("result")) {
+ // result can be a Task, Message, TaskStatusUpdateEvent, or TaskArtifactUpdateEvent
+ String resultJson = jsonObject.get("result").toString();
+ StreamingEventKind_v0_3 event = JsonUtil_v0_3.fromJson(resultJson, StreamingEventKind_v0_3.class);
+ eventHandler.accept(event);
+ if (event instanceof TaskStatusUpdateEvent_v0_3 && ((TaskStatusUpdateEvent_v0_3) event).isFinal()) {
+ future.cancel(true); // close SSE channel
+ }
+ } else {
+ throw new IllegalArgumentException("Unknown message type");
+ }
+ }
+
+}
diff --git a/compat-0.3/client/transport/jsonrpc/src/main/resources/META-INF/services/org.a2aproject.sdk.compat03.client.transport.spi.ClientTransportProvider_v0_3 b/compat-0.3/client/transport/jsonrpc/src/main/resources/META-INF/services/org.a2aproject.sdk.compat03.client.transport.spi.ClientTransportProvider_v0_3
new file mode 100644
index 000000000..baf961ea6
--- /dev/null
+++ b/compat-0.3/client/transport/jsonrpc/src/main/resources/META-INF/services/org.a2aproject.sdk.compat03.client.transport.spi.ClientTransportProvider_v0_3
@@ -0,0 +1 @@
+org.a2aproject.sdk.compat03.client.transport.jsonrpc.JSONRPCTransportProvider_v0_3
\ No newline at end of file
diff --git a/compat-0.3/client/transport/jsonrpc/src/test/java/org/a2aproject/sdk/compat03/client/transport/jsonrpc/JSONRPCTransportStreaming_v0_3_Test.java b/compat-0.3/client/transport/jsonrpc/src/test/java/org/a2aproject/sdk/compat03/client/transport/jsonrpc/JSONRPCTransportStreaming_v0_3_Test.java
new file mode 100644
index 000000000..914b92078
--- /dev/null
+++ b/compat-0.3/client/transport/jsonrpc/src/test/java/org/a2aproject/sdk/compat03/client/transport/jsonrpc/JSONRPCTransportStreaming_v0_3_Test.java
@@ -0,0 +1,174 @@
+package org.a2aproject.sdk.compat03.client.transport.jsonrpc;
+
+import static org.a2aproject.sdk.compat03.client.transport.jsonrpc.JsonStreamingMessages_v0_3.SEND_MESSAGE_STREAMING_TEST_REQUEST;
+import static org.a2aproject.sdk.compat03.client.transport.jsonrpc.JsonStreamingMessages_v0_3.SEND_MESSAGE_STREAMING_TEST_RESPONSE;
+import static org.a2aproject.sdk.compat03.client.transport.jsonrpc.JsonStreamingMessages_v0_3.TASK_RESUBSCRIPTION_REQUEST_TEST_RESPONSE;
+import static org.a2aproject.sdk.compat03.client.transport.jsonrpc.JsonStreamingMessages_v0_3.TASK_RESUBSCRIPTION_TEST_REQUEST;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockserver.model.HttpRequest.request;
+import static org.mockserver.model.HttpResponse.response;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Consumer;
+
+import org.a2aproject.sdk.compat03.spec.Artifact_v0_3;
+import org.a2aproject.sdk.compat03.spec.Message_v0_3;
+import org.a2aproject.sdk.compat03.spec.MessageSendConfiguration_v0_3;
+import org.a2aproject.sdk.compat03.spec.MessageSendParams_v0_3;
+import org.a2aproject.sdk.compat03.spec.Part_v0_3;
+import org.a2aproject.sdk.compat03.spec.StreamingEventKind_v0_3;
+import org.a2aproject.sdk.compat03.spec.Task_v0_3;
+import org.a2aproject.sdk.compat03.spec.TaskIdParams_v0_3;
+import org.a2aproject.sdk.compat03.spec.TaskState_v0_3;
+import org.a2aproject.sdk.compat03.spec.TextPart_v0_3;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockserver.integration.ClientAndServer;
+import org.mockserver.matchers.MatchType;
+import org.mockserver.model.JsonBody;
+
+public class JSONRPCTransportStreaming_v0_3_Test {
+
+ private ClientAndServer server;
+
+ @BeforeEach
+ public void setUp() {
+ server = new ClientAndServer(4001);
+ }
+
+ @AfterEach
+ public void tearDown() {
+ server.stop();
+ }
+
+ @Test
+ public void testSendStreamingMessageParams() {
+ // The goal here is just to verify the correct parameters are being used
+ // This is a unit test of the parameter construction, not the streaming itself
+ Message_v0_3 message = new Message_v0_3.Builder()
+ .role(Message_v0_3.Role.USER)
+ .parts(Collections.singletonList(new TextPart_v0_3("test message")))
+ .contextId("context-test")
+ .messageId("message-test")
+ .build();
+
+ MessageSendConfiguration_v0_3 configuration = new MessageSendConfiguration_v0_3.Builder()
+ .acceptedOutputModes(List.of("text"))
+ .blocking(false)
+ .build();
+
+ MessageSendParams_v0_3 params = new MessageSendParams_v0_3.Builder()
+ .message(message)
+ .configuration(configuration)
+ .build();
+
+ assertNotNull(params);
+ assertEquals(message, params.message());
+ assertEquals(configuration, params.configuration());
+ assertEquals(Message_v0_3.Role.USER, params.message().getRole());
+ assertEquals("test message", ((TextPart_v0_3) params.message().getParts().get(0)).getText());
+ }
+
+ @Test
+ public void testA2AClientSendStreamingMessage() throws Exception {
+ this.server.when(
+ request()
+ .withMethod("POST")
+ .withPath("/")
+ .withBody(JsonBody.json(SEND_MESSAGE_STREAMING_TEST_REQUEST, MatchType.ONLY_MATCHING_FIELDS))
+
+ )
+ .respond(
+ response()
+ .withStatusCode(200)
+ .withHeader("Content-Type", "text/event-stream")
+ .withBody(SEND_MESSAGE_STREAMING_TEST_RESPONSE)
+ );
+
+ JSONRPCTransport_v0_3 client = new JSONRPCTransport_v0_3("http://localhost:4001");
+ Message_v0_3 message = new Message_v0_3.Builder()
+ .role(Message_v0_3.Role.USER)
+ .parts(Collections.singletonList(new TextPart_v0_3("tell me some jokes")))
+ .contextId("context-1234")
+ .messageId("message-1234")
+ .build();
+ MessageSendConfiguration_v0_3 configuration = new MessageSendConfiguration_v0_3.Builder()
+ .acceptedOutputModes(List.of("text"))
+ .blocking(false)
+ .build();
+ MessageSendParams_v0_3 params = new MessageSendParams_v0_3.Builder()
+ .message(message)
+ .configuration(configuration)
+ .build();
+
+ AtomicReference receivedEvent = new AtomicReference<>();
+ CountDownLatch latch = new CountDownLatch(1);
+ Consumer eventHandler = event -> {
+ receivedEvent.set(event);
+ latch.countDown();
+ };
+ Consumer errorHandler = error -> {};
+ client.sendMessageStreaming(params, eventHandler, errorHandler, null);
+
+ boolean eventReceived = latch.await(10, TimeUnit.SECONDS);
+ assertTrue(eventReceived);
+ assertNotNull(receivedEvent.get());
+ }
+
+ @Test
+ public void testA2AClientResubscribeToTask() throws Exception {
+ this.server.when(
+ request()
+ .withMethod("POST")
+ .withPath("/")
+ .withBody(JsonBody.json(TASK_RESUBSCRIPTION_TEST_REQUEST, MatchType.ONLY_MATCHING_FIELDS))
+
+ )
+ .respond(
+ response()
+ .withStatusCode(200)
+ .withHeader("Content-Type", "text/event-stream")
+ .withBody(TASK_RESUBSCRIPTION_REQUEST_TEST_RESPONSE)
+ );
+
+ JSONRPCTransport_v0_3 client = new JSONRPCTransport_v0_3("http://localhost:4001");
+ TaskIdParams_v0_3 taskIdParams = new TaskIdParams_v0_3("task-1234");
+
+ AtomicReference receivedEvent = new AtomicReference<>();
+ CountDownLatch latch = new CountDownLatch(1);
+ Consumer eventHandler = event -> {
+ receivedEvent.set(event);
+ latch.countDown();
+ };
+ Consumer errorHandler = error -> {};
+ client.resubscribe(taskIdParams, eventHandler, errorHandler, null);
+
+ boolean eventReceived = latch.await(10, TimeUnit.SECONDS);
+ assertTrue(eventReceived);
+
+ StreamingEventKind_v0_3 eventKind = receivedEvent.get();;
+ assertNotNull(eventKind);
+ assertInstanceOf(Task_v0_3.class, eventKind);
+ Task_v0_3 task = (Task_v0_3) eventKind;
+ assertEquals("2", task.getId());
+ assertEquals("context-1234", task.getContextId());
+ assertEquals(TaskState_v0_3.COMPLETED, task.getStatus().state());
+ List artifacts = task.getArtifacts();
+ assertEquals(1, artifacts.size());
+ Artifact_v0_3 artifact = artifacts.get(0);
+ assertEquals("artifact-1", artifact.artifactId());
+ assertEquals("joke", artifact.name());
+ Part_v0_3> part = artifact.parts().get(0);
+ assertEquals(Part_v0_3.Kind.TEXT, part.getKind());
+ assertEquals("Why did the chicken cross the road? To get to the other side!", ((TextPart_v0_3) part).getText());
+ }
+}
\ No newline at end of file
diff --git a/compat-0.3/client/transport/jsonrpc/src/test/java/org/a2aproject/sdk/compat03/client/transport/jsonrpc/JSONRPCTransport_v0_3_Test.java b/compat-0.3/client/transport/jsonrpc/src/test/java/org/a2aproject/sdk/compat03/client/transport/jsonrpc/JSONRPCTransport_v0_3_Test.java
new file mode 100644
index 000000000..b3ca67f9e
--- /dev/null
+++ b/compat-0.3/client/transport/jsonrpc/src/test/java/org/a2aproject/sdk/compat03/client/transport/jsonrpc/JSONRPCTransport_v0_3_Test.java
@@ -0,0 +1,683 @@
+package org.a2aproject.sdk.compat03.client.transport.jsonrpc;
+
+import static org.a2aproject.sdk.compat03.client.transport.jsonrpc.JsonMessages_v0_3.AGENT_CARD;
+import static org.a2aproject.sdk.compat03.client.transport.jsonrpc.JsonMessages_v0_3.AGENT_CARD_SUPPORTS_EXTENDED;
+import static org.a2aproject.sdk.compat03.client.transport.jsonrpc.JsonMessages_v0_3.CANCEL_TASK_TEST_REQUEST;
+import static org.a2aproject.sdk.compat03.client.transport.jsonrpc.JsonMessages_v0_3.CANCEL_TASK_TEST_RESPONSE;
+import static org.a2aproject.sdk.compat03.client.transport.jsonrpc.JsonMessages_v0_3.GET_AUTHENTICATED_EXTENDED_AGENT_CARD_REQUEST;
+import static org.a2aproject.sdk.compat03.client.transport.jsonrpc.JsonMessages_v0_3.GET_AUTHENTICATED_EXTENDED_AGENT_CARD_RESPONSE;
+import static org.a2aproject.sdk.compat03.client.transport.jsonrpc.JsonMessages_v0_3.GET_TASK_PUSH_NOTIFICATION_CONFIG_TEST_REQUEST;
+import static org.a2aproject.sdk.compat03.client.transport.jsonrpc.JsonMessages_v0_3.GET_TASK_PUSH_NOTIFICATION_CONFIG_TEST_RESPONSE;
+import static org.a2aproject.sdk.compat03.client.transport.jsonrpc.JsonMessages_v0_3.GET_TASK_TEST_REQUEST;
+import static org.a2aproject.sdk.compat03.client.transport.jsonrpc.JsonMessages_v0_3.GET_TASK_TEST_RESPONSE;
+import static org.a2aproject.sdk.compat03.client.transport.jsonrpc.JsonMessages_v0_3.SEND_MESSAGE_ERROR_TEST_RESPONSE;
+import static org.a2aproject.sdk.compat03.client.transport.jsonrpc.JsonMessages_v0_3.SEND_MESSAGE_TEST_REQUEST;
+import static org.a2aproject.sdk.compat03.client.transport.jsonrpc.JsonMessages_v0_3.SEND_MESSAGE_TEST_REQUEST_WITH_MESSAGE_RESPONSE;
+import static org.a2aproject.sdk.compat03.client.transport.jsonrpc.JsonMessages_v0_3.SEND_MESSAGE_TEST_RESPONSE;
+import static org.a2aproject.sdk.compat03.client.transport.jsonrpc.JsonMessages_v0_3.SEND_MESSAGE_TEST_RESPONSE_WITH_MESSAGE_RESPONSE;
+import static org.a2aproject.sdk.compat03.client.transport.jsonrpc.JsonMessages_v0_3.SEND_MESSAGE_WITH_DATA_PART_TEST_REQUEST;
+import static org.a2aproject.sdk.compat03.client.transport.jsonrpc.JsonMessages_v0_3.SEND_MESSAGE_WITH_DATA_PART_TEST_RESPONSE;
+import static org.a2aproject.sdk.compat03.client.transport.jsonrpc.JsonMessages_v0_3.SEND_MESSAGE_WITH_ERROR_TEST_REQUEST;
+import static org.a2aproject.sdk.compat03.client.transport.jsonrpc.JsonMessages_v0_3.SEND_MESSAGE_WITH_FILE_PART_TEST_REQUEST;
+import static org.a2aproject.sdk.compat03.client.transport.jsonrpc.JsonMessages_v0_3.SEND_MESSAGE_WITH_FILE_PART_TEST_RESPONSE;
+import static org.a2aproject.sdk.compat03.client.transport.jsonrpc.JsonMessages_v0_3.SEND_MESSAGE_WITH_MIXED_PARTS_TEST_REQUEST;
+import static org.a2aproject.sdk.compat03.client.transport.jsonrpc.JsonMessages_v0_3.SEND_MESSAGE_WITH_MIXED_PARTS_TEST_RESPONSE;
+import static org.a2aproject.sdk.compat03.client.transport.jsonrpc.JsonMessages_v0_3.SET_TASK_PUSH_NOTIFICATION_CONFIG_TEST_REQUEST;
+import static org.a2aproject.sdk.compat03.client.transport.jsonrpc.JsonMessages_v0_3.SET_TASK_PUSH_NOTIFICATION_CONFIG_TEST_RESPONSE;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+import static org.mockserver.model.HttpRequest.request;
+import static org.mockserver.model.HttpResponse.response;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.a2aproject.sdk.compat03.spec.A2AClientException_v0_3;
+import org.a2aproject.sdk.compat03.spec.AgentCard_v0_3;
+import org.a2aproject.sdk.compat03.spec.AgentInterface_v0_3;
+import org.a2aproject.sdk.compat03.spec.AgentSkill_v0_3;
+import org.a2aproject.sdk.compat03.spec.Artifact_v0_3;
+import org.a2aproject.sdk.compat03.spec.DataPart_v0_3;
+import org.a2aproject.sdk.compat03.spec.EventKind_v0_3;
+import org.a2aproject.sdk.compat03.spec.FileContent_v0_3;
+import org.a2aproject.sdk.compat03.spec.FilePart_v0_3;
+import org.a2aproject.sdk.compat03.spec.FileWithBytes_v0_3;
+import org.a2aproject.sdk.compat03.spec.FileWithUri_v0_3;
+import org.a2aproject.sdk.compat03.spec.GetTaskPushNotificationConfigParams_v0_3;
+import org.a2aproject.sdk.compat03.spec.Message_v0_3;
+import org.a2aproject.sdk.compat03.spec.MessageSendConfiguration_v0_3;
+import org.a2aproject.sdk.compat03.spec.MessageSendParams_v0_3;
+import org.a2aproject.sdk.compat03.spec.OpenIdConnectSecurityScheme_v0_3;
+import org.a2aproject.sdk.compat03.spec.Part_v0_3;
+import org.a2aproject.sdk.compat03.spec.PushNotificationAuthenticationInfo_v0_3;
+import org.a2aproject.sdk.compat03.spec.PushNotificationConfig_v0_3;
+import org.a2aproject.sdk.compat03.spec.SecurityScheme_v0_3;
+import org.a2aproject.sdk.compat03.spec.Task_v0_3;
+import org.a2aproject.sdk.compat03.spec.TaskIdParams_v0_3;
+import org.a2aproject.sdk.compat03.spec.TaskPushNotificationConfig_v0_3;
+import org.a2aproject.sdk.compat03.spec.TaskQueryParams_v0_3;
+import org.a2aproject.sdk.compat03.spec.TaskState_v0_3;
+import org.a2aproject.sdk.compat03.spec.TextPart_v0_3;
+import org.a2aproject.sdk.compat03.spec.TransportProtocol_v0_3;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockserver.integration.ClientAndServer;
+import org.mockserver.matchers.MatchType;
+import org.mockserver.model.JsonBody;
+
+public class JSONRPCTransport_v0_3_Test {
+
+ private ClientAndServer server;
+
+ @BeforeEach
+ public void setUp() {
+ server = new ClientAndServer(4001);
+ }
+
+ @AfterEach
+ public void tearDown() {
+ server.stop();
+ }
+
+ @Test
+ public void testA2AClientSendMessage() throws Exception {
+ this.server.when(
+ request()
+ .withMethod("POST")
+ .withPath("/")
+ .withBody(JsonBody.json(SEND_MESSAGE_TEST_REQUEST, MatchType.ONLY_MATCHING_FIELDS))
+
+ )
+ .respond(
+ response()
+ .withStatusCode(200)
+ .withBody(SEND_MESSAGE_TEST_RESPONSE)
+ );
+
+ JSONRPCTransport_v0_3 client = new JSONRPCTransport_v0_3("http://localhost:4001");
+ Message_v0_3 message = new Message_v0_3.Builder()
+ .role(Message_v0_3.Role.USER)
+ .parts(Collections.singletonList(new TextPart_v0_3("tell me a joke")))
+ .contextId("context-1234")
+ .messageId("message-1234")
+ .build();
+ MessageSendConfiguration_v0_3 configuration = new MessageSendConfiguration_v0_3.Builder()
+ .acceptedOutputModes(List.of("text"))
+ .blocking(true)
+ .build();
+ MessageSendParams_v0_3 params = new MessageSendParams_v0_3.Builder()
+ .message(message)
+ .configuration(configuration)
+ .build();
+
+ EventKind_v0_3 result = client.sendMessage(params, null);
+ assertInstanceOf(Task_v0_3.class, result);
+ Task_v0_3 task = (Task_v0_3) result;
+ assertEquals("de38c76d-d54c-436c-8b9f-4c2703648d64", task.getId());
+ assertNotNull(task.getContextId());
+ assertEquals(TaskState_v0_3.COMPLETED,task.getStatus().state());
+ assertEquals(1, task.getArtifacts().size());
+ Artifact_v0_3 artifact = task.getArtifacts().get(0);
+ assertEquals("artifact-1", artifact.artifactId());
+ assertEquals("joke", artifact.name());
+ assertEquals(1, artifact.parts().size());
+ Part_v0_3> part = artifact.parts().get(0);
+ assertEquals(Part_v0_3.Kind.TEXT, part.getKind());
+ assertEquals("Why did the chicken cross the road? To get to the other side!", ((TextPart_v0_3) part).getText());
+ assertTrue(task.getMetadata().isEmpty());
+ }
+
+ @Test
+ public void testA2AClientSendMessageWithMessageResponse() throws Exception {
+ this.server.when(
+ request()
+ .withMethod("POST")
+ .withPath("/")
+ .withBody(JsonBody.json(SEND_MESSAGE_TEST_REQUEST_WITH_MESSAGE_RESPONSE, MatchType.ONLY_MATCHING_FIELDS))
+
+ )
+ .respond(
+ response()
+ .withStatusCode(200)
+ .withBody(SEND_MESSAGE_TEST_RESPONSE_WITH_MESSAGE_RESPONSE)
+ );
+
+ JSONRPCTransport_v0_3 client = new JSONRPCTransport_v0_3("http://localhost:4001");
+ Message_v0_3 message = new Message_v0_3.Builder()
+ .role(Message_v0_3.Role.USER)
+ .parts(Collections.singletonList(new TextPart_v0_3("tell me a joke")))
+ .contextId("context-1234")
+ .messageId("message-1234")
+ .build();
+ MessageSendConfiguration_v0_3 configuration = new MessageSendConfiguration_v0_3.Builder()
+ .acceptedOutputModes(List.of("text"))
+ .blocking(true)
+ .build();
+ MessageSendParams_v0_3 params = new MessageSendParams_v0_3.Builder()
+ .message(message)
+ .configuration(configuration)
+ .build();
+
+ EventKind_v0_3 result = client.sendMessage(params, null);
+ assertInstanceOf(Message_v0_3.class, result);
+ Message_v0_3 agentMessage = (Message_v0_3) result;
+ assertEquals(Message_v0_3.Role.AGENT, agentMessage.getRole());
+ Part_v0_3> part = agentMessage.getParts().get(0);
+ assertEquals(Part_v0_3.Kind.TEXT, part.getKind());
+ assertEquals("Why did the chicken cross the road? To get to the other side!", ((TextPart_v0_3) part).getText());
+ assertEquals("msg-456", agentMessage.getMessageId());
+ }
+
+
+ @Test
+ public void testA2AClientSendMessageWithError() throws Exception {
+ this.server.when(
+ request()
+ .withMethod("POST")
+ .withPath("/")
+ .withBody(JsonBody.json(SEND_MESSAGE_WITH_ERROR_TEST_REQUEST, MatchType.ONLY_MATCHING_FIELDS))
+
+ )
+ .respond(
+ response()
+ .withStatusCode(200)
+ .withBody(SEND_MESSAGE_ERROR_TEST_RESPONSE)
+ );
+
+ JSONRPCTransport_v0_3 client = new JSONRPCTransport_v0_3("http://localhost:4001");
+ Message_v0_3 message = new Message_v0_3.Builder()
+ .role(Message_v0_3.Role.USER)
+ .parts(Collections.singletonList(new TextPart_v0_3("tell me a joke")))
+ .contextId("context-1234")
+ .messageId("message-1234")
+ .build();
+ MessageSendConfiguration_v0_3 configuration = new MessageSendConfiguration_v0_3.Builder()
+ .acceptedOutputModes(List.of("text"))
+ .blocking(true)
+ .build();
+ MessageSendParams_v0_3 params = new MessageSendParams_v0_3.Builder()
+ .message(message)
+ .configuration(configuration)
+ .build();
+
+ try {
+ client.sendMessage(params, null);
+ fail(); // should not reach here
+ } catch (A2AClientException_v0_3 e) {
+ assertTrue(e.getMessage().contains("Invalid parameters: Hello world"));
+ }
+ }
+
+ @Test
+ public void testA2AClientGetTask() throws Exception {
+ this.server.when(
+ request()
+ .withMethod("POST")
+ .withPath("/")
+ .withBody(JsonBody.json(GET_TASK_TEST_REQUEST, MatchType.ONLY_MATCHING_FIELDS))
+
+ )
+ .respond(
+ response()
+ .withStatusCode(200)
+ .withBody(GET_TASK_TEST_RESPONSE)
+ );
+
+ JSONRPCTransport_v0_3 client = new JSONRPCTransport_v0_3("http://localhost:4001");
+ Task_v0_3 task = client.getTask(new TaskQueryParams_v0_3("de38c76d-d54c-436c-8b9f-4c2703648d64",
+ 10), null);
+ assertEquals("de38c76d-d54c-436c-8b9f-4c2703648d64", task.getId());
+ assertEquals("c295ea44-7543-4f78-b524-7a38915ad6e4", task.getContextId());
+ assertEquals(TaskState_v0_3.COMPLETED, task.getStatus().state());
+ assertEquals(1, task.getArtifacts().size());
+ Artifact_v0_3 artifact = task.getArtifacts().get(0);
+ assertEquals(1, artifact.parts().size());
+ assertEquals("artifact-1", artifact.artifactId());
+ Part_v0_3> part = artifact.parts().get(0);
+ assertEquals(Part_v0_3.Kind.TEXT, part.getKind());
+ assertEquals("Why did the chicken cross the road? To get to the other side!", ((TextPart_v0_3) part).getText());
+ assertTrue(task.getMetadata().isEmpty());
+ List history = task.getHistory();
+ assertNotNull(history);
+ assertEquals(1, history.size());
+ Message_v0_3 message = history.get(0);
+ assertEquals(Message_v0_3.Role.USER, message.getRole());
+ List> parts = message.getParts();
+ assertNotNull(parts);
+ assertEquals(3, parts.size());
+ part = parts.get(0);
+ assertEquals(Part_v0_3.Kind.TEXT, part.getKind());
+ assertEquals("tell me a joke", ((TextPart_v0_3)part).getText());
+ part = parts.get(1);
+ assertEquals(Part_v0_3.Kind.FILE, part.getKind());
+ FileContent_v0_3 filePart = ((FilePart_v0_3) part).getFile();
+ assertEquals("file:///path/to/file.txt", ((FileWithUri_v0_3) filePart).uri());
+ assertEquals("text/plain", filePart.mimeType());
+ part = parts.get(2);
+ assertEquals(Part_v0_3.Kind.FILE, part.getKind());
+ filePart = ((FilePart_v0_3) part).getFile();
+ assertEquals("aGVsbG8=", ((FileWithBytes_v0_3) filePart).bytes());
+ assertEquals("hello.txt", filePart.name());
+ assertTrue(task.getMetadata().isEmpty());
+ }
+
+ @Test
+ public void testA2AClientCancelTask() throws Exception {
+ this.server.when(
+ request()
+ .withMethod("POST")
+ .withPath("/")
+ .withBody(JsonBody.json(CANCEL_TASK_TEST_REQUEST, MatchType.ONLY_MATCHING_FIELDS))
+
+ )
+ .respond(
+ response()
+ .withStatusCode(200)
+ .withBody(CANCEL_TASK_TEST_RESPONSE)
+ );
+
+ JSONRPCTransport_v0_3 client = new JSONRPCTransport_v0_3("http://localhost:4001");
+ Task_v0_3 task = client.cancelTask(new TaskIdParams_v0_3("de38c76d-d54c-436c-8b9f-4c2703648d64",
+ new HashMap<>()), null);
+ assertEquals("de38c76d-d54c-436c-8b9f-4c2703648d64", task.getId());
+ assertEquals("c295ea44-7543-4f78-b524-7a38915ad6e4", task.getContextId());
+ assertEquals(TaskState_v0_3.CANCELED, task.getStatus().state());
+ assertTrue(task.getMetadata().isEmpty());
+ }
+
+ @Test
+ public void testA2AClientGetTaskPushNotificationConfig() throws Exception {
+ this.server.when(
+ request()
+ .withMethod("POST")
+ .withPath("/")
+ .withBody(JsonBody.json(GET_TASK_PUSH_NOTIFICATION_CONFIG_TEST_REQUEST, MatchType.ONLY_MATCHING_FIELDS))
+
+ )
+ .respond(
+ response()
+ .withStatusCode(200)
+ .withBody(GET_TASK_PUSH_NOTIFICATION_CONFIG_TEST_RESPONSE)
+ );
+
+ JSONRPCTransport_v0_3 client = new JSONRPCTransport_v0_3("http://localhost:4001");
+ TaskPushNotificationConfig_v0_3 taskPushNotificationConfig = client.getTaskPushNotificationConfiguration(
+ new GetTaskPushNotificationConfigParams_v0_3("de38c76d-d54c-436c-8b9f-4c2703648d64", null,
+ new HashMap<>()), null);
+ PushNotificationConfig_v0_3 pushNotificationConfig = taskPushNotificationConfig.pushNotificationConfig();
+ assertNotNull(pushNotificationConfig);
+ assertEquals("https://example.com/callback", pushNotificationConfig.url());
+ PushNotificationAuthenticationInfo_v0_3 authenticationInfo = pushNotificationConfig.authentication();
+ assertTrue(authenticationInfo.schemes().size() == 1);
+ assertEquals("jwt", authenticationInfo.schemes().get(0));
+ }
+
+ @Test
+ public void testA2AClientSetTaskPushNotificationConfig() throws Exception {
+ this.server.when(
+ request()
+ .withMethod("POST")
+ .withPath("/")
+ .withBody(JsonBody.json(SET_TASK_PUSH_NOTIFICATION_CONFIG_TEST_REQUEST, MatchType.ONLY_MATCHING_FIELDS))
+
+ )
+ .respond(
+ response()
+ .withStatusCode(200)
+ .withBody(SET_TASK_PUSH_NOTIFICATION_CONFIG_TEST_RESPONSE)
+ );
+
+ JSONRPCTransport_v0_3 client = new JSONRPCTransport_v0_3("http://localhost:4001");
+ TaskPushNotificationConfig_v0_3 taskPushNotificationConfig = client.setTaskPushNotificationConfiguration(
+ new TaskPushNotificationConfig_v0_3("de38c76d-d54c-436c-8b9f-4c2703648d64",
+ new PushNotificationConfig_v0_3.Builder()
+ .url("https://example.com/callback")
+ .authenticationInfo(new PushNotificationAuthenticationInfo_v0_3(Collections.singletonList("jwt"),
+ null))
+ .build()), null);
+ PushNotificationConfig_v0_3 pushNotificationConfig = taskPushNotificationConfig.pushNotificationConfig();
+ assertNotNull(pushNotificationConfig);
+ assertEquals("https://example.com/callback", pushNotificationConfig.url());
+ PushNotificationAuthenticationInfo_v0_3 authenticationInfo = pushNotificationConfig.authentication();
+ assertEquals(1, authenticationInfo.schemes().size());
+ assertEquals("jwt", authenticationInfo.schemes().get(0));
+ }
+
+
+ @Test
+ public void testA2AClientGetAgentCard() throws Exception {
+ this.server.when(
+ request()
+ .withMethod("GET")
+ .withPath("/.well-known/agent-card.json")
+ )
+ .respond(
+ response()
+ .withStatusCode(200)
+ .withBody(AGENT_CARD)
+ );
+
+ JSONRPCTransport_v0_3 client = new JSONRPCTransport_v0_3("http://localhost:4001");
+ AgentCard_v0_3 agentCard = client.getAgentCard(null);
+ assertEquals("GeoSpatial Route Planner Agent", agentCard.name());
+ assertEquals("Provides advanced route planning, traffic analysis, and custom map generation services. This agent can calculate optimal routes, estimate travel times considering real-time traffic, and create personalized maps with points of interest.", agentCard.description());
+ assertEquals("https://georoute-agent.example.com/a2a/v1", agentCard.url());
+ assertEquals("Example Geo Services Inc.", agentCard.provider().organization());
+ assertEquals("https://www.examplegeoservices.com", agentCard.provider().url());
+ assertEquals("1.2.0", agentCard.version());
+ assertEquals("https://docs.examplegeoservices.com/georoute-agent/api", agentCard.documentationUrl());
+ assertTrue(agentCard.capabilities().streaming());
+ assertTrue(agentCard.capabilities().pushNotifications());
+ assertFalse(agentCard.capabilities().stateTransitionHistory());
+ Map securitySchemes = agentCard.securitySchemes();
+ assertNotNull(securitySchemes);
+ OpenIdConnectSecurityScheme_v0_3 google = (OpenIdConnectSecurityScheme_v0_3) securitySchemes.get("google");
+ assertEquals("openIdConnect", google.getType());
+ assertEquals("https://accounts.google.com/.well-known/openid-configuration", google.getOpenIdConnectUrl());
+ List