Skip to content

Commit a9e52e2

Browse files
kvmiloscopybara-github
authored andcommitted
feat(a2a): add an A2A v1.0 client with Android support and deprecate the v0.3 JvmA2AAgent
PiperOrigin-RevId: 940379674
1 parent c94daef commit a9e52e2

32 files changed

Lines changed: 3775 additions & 424 deletions

File tree

a2a/build.gradle.kts

Lines changed: 43 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,17 @@
1616

1717
plugins {
1818
kotlin("multiplatform")
19+
id("com.android.kotlin.multiplatform.library")
1920
id("maven-publish")
2021
}
2122

2223
kotlin {
24+
// AGP 9 KMP Android library target (replaces com.android.library + androidTarget).
25+
android {
26+
namespace = "com.google.adk.a2a"
27+
compileSdk = rootProject.extra["androidCompileSdk"] as Int
28+
minSdk = rootProject.extra["androidMinSdk"] as Int
29+
}
2330
jvm()
2431

2532
sourceSets {
@@ -29,29 +36,58 @@ kotlin {
2936
implementation(libs.kotlinx.coroutines.core)
3037
}
3138
}
32-
val commonTest by getting { dependencies { implementation(kotlin("test")) } }
39+
val commonTest by getting {
40+
dependencies {
41+
implementation(project(":google-adk-kotlin-testing"))
42+
implementation(kotlin("test"))
43+
}
44+
}
3345
val commonJvmAndroidMain by creating {
3446
dependsOn(commonMain)
3547
dependencies {
48+
implementation(libs.kotlinx.serialization)
3649
implementation(libs.jackson.databind)
3750
implementation(libs.jackson.datatype.jsr310)
38-
implementation(libs.jackson.module.kotlin)
3951
implementation(libs.a2a.sdk.client)
4052
implementation(libs.a2a.sdk.common)
4153
implementation(libs.a2a.sdk.spec)
4254
implementation(libs.a2a.sdk.transport.rest)
4355
}
4456
}
45-
val jvmMain by getting { dependsOn(commonJvmAndroidMain) }
57+
// jvmMain hosts the deprecated v0.3 path (JVM-only); androidMain stays v1.0-only.
58+
val jvmMain by getting {
59+
dependsOn(commonJvmAndroidMain)
60+
dependencies {
61+
// Only the deprecated v0.3 (JVM-only) converters use jackson-module-kotlin; keep it off the
62+
// Android path, which serializes via kotlinx.serialization instead.
63+
implementation(libs.jackson.module.kotlin)
64+
implementation(libs.a2a.legacy.sdk.client)
65+
implementation(libs.a2a.legacy.sdk.common)
66+
implementation(libs.a2a.legacy.sdk.spec)
67+
}
68+
}
69+
val jvmTest by getting {
70+
dependencies {
71+
implementation(libs.junit)
72+
implementation(libs.google.truth)
73+
implementation(libs.mockito.kotlin)
74+
implementation(libs.kotlinx.coroutines.test)
75+
implementation(libs.a2a.legacy.sdk.client)
76+
implementation(libs.a2a.legacy.sdk.spec)
77+
}
78+
}
79+
val androidMain by getting { dependsOn(commonJvmAndroidMain) }
4680
}
4781
}
4882

4983
// Coordinates the Kotlin Multiplatform plugin uses for the publications it
5084
// auto-creates:
51-
// - `kotlinMultiplatform` -> google-adk-kotlin-a2a (root metadata)
52-
// - `jvm` -> google-adk-kotlin-a2a-jvm (KMP target)
53-
// POM metadata, Dokka javadoc, and GPG signing are configured in the root
54-
// build.gradle.kts.
85+
// - `kotlinMultiplatform` -> google-adk-kotlin-a2a (root metadata)
86+
// - `jvm` -> google-adk-kotlin-a2a-jvm (KMP target)
87+
// - `androidRelease` -> google-adk-kotlin-a2a-android (KMP target)
88+
// Per-target suffixes (`-jvm`, `-android`) are appended by the KMP plugin
89+
// automatically. POM metadata, Dokka javadoc, and GPG signing are configured in
90+
// the root build.gradle.kts.
5591
publishing {
5692
publications.withType<MavenPublication>().configureEach {
5793
if (name == "kotlinMultiplatform") {
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
com.google.adk.kt.a2a.android.JsonRpcHttpClientTransportProvider
Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
/*
2+
* Copyright 2026 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.adk.kt.a2a.android;
18+
19+
import com.google.gson.JsonElement;
20+
import com.google.gson.JsonObject;
21+
import com.google.gson.JsonParser;
22+
import java.util.function.Consumer;
23+
import org.a2aproject.sdk.client.http.A2AHttpClient;
24+
import org.a2aproject.sdk.client.http.A2AHttpResponse;
25+
import org.a2aproject.sdk.client.transport.spi.ClientTransport;
26+
import org.a2aproject.sdk.client.transport.spi.interceptors.ClientCallContext;
27+
import org.a2aproject.sdk.jsonrpc.common.json.JsonProcessingException;
28+
import org.a2aproject.sdk.jsonrpc.common.json.JsonUtil;
29+
import org.a2aproject.sdk.jsonrpc.common.wrappers.ListTasksResult;
30+
import org.a2aproject.sdk.jsonrpc.common.wrappers.SendMessageRequest;
31+
import org.a2aproject.sdk.spec.A2AClientException;
32+
import org.a2aproject.sdk.spec.AgentCard;
33+
import org.a2aproject.sdk.spec.CancelTaskParams;
34+
import org.a2aproject.sdk.spec.DeleteTaskPushNotificationConfigParams;
35+
import org.a2aproject.sdk.spec.EventKind;
36+
import org.a2aproject.sdk.spec.GetExtendedAgentCardParams;
37+
import org.a2aproject.sdk.spec.GetTaskPushNotificationConfigParams;
38+
import org.a2aproject.sdk.spec.ListTaskPushNotificationConfigsParams;
39+
import org.a2aproject.sdk.spec.ListTaskPushNotificationConfigsResult;
40+
import org.a2aproject.sdk.spec.ListTasksParams;
41+
import org.a2aproject.sdk.spec.MessageSendParams;
42+
import org.a2aproject.sdk.spec.StreamingEventKind;
43+
import org.a2aproject.sdk.spec.Task;
44+
import org.a2aproject.sdk.spec.TaskIdParams;
45+
import org.a2aproject.sdk.spec.TaskPushNotificationConfig;
46+
import org.a2aproject.sdk.spec.TaskQueryParams;
47+
import org.jspecify.annotations.Nullable;
48+
49+
/**
50+
* A proto-free {@link ClientTransport} that runs a real non-streaming {@code message/send} JSON-RPC
51+
* round-trip over an injected {@link A2AHttpClient}.
52+
*
53+
* <p>The SDK's {@code JSONRPCTransport} marshals through protobuf and isn't Android-buildable, so
54+
* this reuses the SDK's proto-free {@code jsonrpccommon} {@link JsonUtil} to serialize the request
55+
* and parse the response, proving a proto-free A2A transport works on Android. The remaining {@link
56+
* ClientTransport} methods throw {@link UnsupportedOperationException}.
57+
*/
58+
public final class JsonRpcHttpClientTransport implements ClientTransport {
59+
60+
private static final String JSONRPC_VERSION = "2.0";
61+
private static final String REQUEST_ID = "1";
62+
private static final String A2A_VERSION_HEADER = "A2A-Version";
63+
private static final String A2A_VERSION = "1.0";
64+
65+
private final A2AHttpClient httpClient;
66+
private final String url;
67+
68+
public JsonRpcHttpClientTransport(A2AHttpClient httpClient, String url) {
69+
this.httpClient = httpClient;
70+
this.url = url;
71+
}
72+
73+
@Override
74+
public EventKind sendMessage(MessageSendParams request, @Nullable ClientCallContext context) {
75+
// Serialize via the SDK's proto-free JSON-RPC machinery (no hand-rolled JSON).
76+
String body;
77+
try {
78+
JsonObject envelope =
79+
JsonParser.parseString(
80+
JsonUtil.toJson(new SendMessageRequest(JSONRPC_VERSION, REQUEST_ID, request)))
81+
.getAsJsonObject();
82+
// JsonUtil wraps params.message via the StreamingEventKind adapter; unwrap it for the wire.
83+
JsonObject params = envelope.getAsJsonObject("params");
84+
params.add("message", params.getAsJsonObject("message").get("message"));
85+
body = envelope.toString();
86+
} catch (JsonProcessingException e) {
87+
throw new A2AClientException("Failed to serialize A2A request", e);
88+
}
89+
90+
try {
91+
A2AHttpResponse response =
92+
httpClient
93+
.createPost()
94+
.url(url)
95+
.addHeader(A2AHttpClient.CONTENT_TYPE, A2AHttpClient.APPLICATION_JSON)
96+
.addHeader(A2A_VERSION_HEADER, A2A_VERSION)
97+
.body(body)
98+
.post();
99+
if (response.status() < 200 || response.status() >= 300) {
100+
throw new A2AClientException("Unexpected HTTP status: " + response.status());
101+
}
102+
return parseSendMessageResponse(response.body());
103+
} catch (A2AClientException e) {
104+
throw e;
105+
} catch (InterruptedException e) {
106+
Thread.currentThread().interrupt();
107+
throw new A2AClientException("Android A2A HTTP round-trip interrupted", e);
108+
} catch (Exception e) {
109+
throw new A2AClientException("Android A2A HTTP round-trip failed", e);
110+
}
111+
}
112+
113+
/** Parses a JSON-RPC {@code message/send} response body into its result {@link EventKind}. */
114+
private static EventKind parseSendMessageResponse(String responseBody) {
115+
JsonObject envelope = JsonParser.parseString(responseBody).getAsJsonObject();
116+
117+
JsonElement errorNode = envelope.get("error");
118+
if (errorNode != null && !errorNode.isJsonNull()) {
119+
throw new A2AClientException("A2A JSON-RPC error: " + errorNode);
120+
}
121+
122+
JsonElement resultNode = envelope.get("result");
123+
if (resultNode == null || !resultNode.isJsonObject()) {
124+
throw new A2AClientException("A2A JSON-RPC response missing 'result' object");
125+
}
126+
127+
try {
128+
// Result is a Task/Message; the SDK's StreamingEventKind adapter picks the concrete type.
129+
return (EventKind) JsonUtil.fromJson(resultNode.toString(), StreamingEventKind.class);
130+
} catch (JsonProcessingException e) {
131+
throw new A2AClientException("Failed to parse A2A response result", e);
132+
}
133+
}
134+
135+
// --- Unused operations -------------------------------------------------------------------------
136+
137+
@Override
138+
public void sendMessageStreaming(
139+
MessageSendParams request,
140+
Consumer<StreamingEventKind> eventConsumer,
141+
Consumer<Throwable> errorConsumer,
142+
@Nullable ClientCallContext context) {
143+
throw new UnsupportedOperationException(
144+
"streaming not supported by JsonRpcHttpClientTransport");
145+
}
146+
147+
@Override
148+
public Task getTask(TaskQueryParams request, @Nullable ClientCallContext context) {
149+
throw new UnsupportedOperationException();
150+
}
151+
152+
@Override
153+
public Task cancelTask(CancelTaskParams request, @Nullable ClientCallContext context) {
154+
throw new UnsupportedOperationException();
155+
}
156+
157+
@Override
158+
public ListTasksResult listTasks(ListTasksParams request, @Nullable ClientCallContext context) {
159+
throw new UnsupportedOperationException();
160+
}
161+
162+
@Override
163+
public TaskPushNotificationConfig createTaskPushNotificationConfiguration(
164+
TaskPushNotificationConfig request, @Nullable ClientCallContext context) {
165+
throw new UnsupportedOperationException();
166+
}
167+
168+
@Override
169+
public TaskPushNotificationConfig getTaskPushNotificationConfiguration(
170+
GetTaskPushNotificationConfigParams request, @Nullable ClientCallContext context) {
171+
throw new UnsupportedOperationException();
172+
}
173+
174+
@Override
175+
public ListTaskPushNotificationConfigsResult listTaskPushNotificationConfigurations(
176+
ListTaskPushNotificationConfigsParams request, @Nullable ClientCallContext context) {
177+
throw new UnsupportedOperationException();
178+
}
179+
180+
@Override
181+
public void deleteTaskPushNotificationConfigurations(
182+
DeleteTaskPushNotificationConfigParams request, @Nullable ClientCallContext context) {
183+
throw new UnsupportedOperationException();
184+
}
185+
186+
@Override
187+
public void subscribeToTask(
188+
TaskIdParams request,
189+
Consumer<StreamingEventKind> eventConsumer,
190+
Consumer<Throwable> errorConsumer,
191+
@Nullable ClientCallContext context) {
192+
throw new UnsupportedOperationException();
193+
}
194+
195+
@Override
196+
public AgentCard getExtendedAgentCard(
197+
GetExtendedAgentCardParams params, @Nullable ClientCallContext context) {
198+
throw new UnsupportedOperationException();
199+
}
200+
201+
@Override
202+
public void close() {}
203+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*
2+
* Copyright 2026 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.adk.kt.a2a.android;
18+
19+
import org.a2aproject.sdk.client.http.A2AHttpClient;
20+
import org.a2aproject.sdk.client.transport.spi.ClientTransportConfig;
21+
22+
/** Config carrying the {@link A2AHttpClient} for {@link JsonRpcHttpClientTransport}. */
23+
public final class JsonRpcHttpClientTransportConfig
24+
extends ClientTransportConfig<JsonRpcHttpClientTransport> {
25+
26+
private final A2AHttpClient httpClient;
27+
28+
public JsonRpcHttpClientTransportConfig(A2AHttpClient httpClient) {
29+
this.httpClient = httpClient;
30+
}
31+
32+
public A2AHttpClient getHttpClient() {
33+
return httpClient;
34+
}
35+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/*
2+
* Copyright 2026 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.adk.kt.a2a.android;
18+
19+
import org.a2aproject.sdk.client.http.AndroidA2AHttpClient;
20+
import org.a2aproject.sdk.client.transport.spi.ClientTransportProvider;
21+
import org.a2aproject.sdk.spec.AgentCard;
22+
import org.a2aproject.sdk.spec.AgentInterface;
23+
import org.a2aproject.sdk.spec.TransportProtocol;
24+
import org.jspecify.annotations.Nullable;
25+
26+
/**
27+
* SPI provider that lets {@code Client.builder(card).withTransport(...)} build a {@link
28+
* JsonRpcHttpClientTransport} without reaching {@code Client}'s package-private constructor.
29+
*/
30+
public final class JsonRpcHttpClientTransportProvider
31+
implements ClientTransportProvider<
32+
JsonRpcHttpClientTransport, JsonRpcHttpClientTransportConfig> {
33+
34+
@Override
35+
public JsonRpcHttpClientTransport create(
36+
@Nullable JsonRpcHttpClientTransportConfig config,
37+
AgentCard agentCard,
38+
AgentInterface agentInterface) {
39+
AndroidA2AHttpClient fallback = new AndroidA2AHttpClient();
40+
return new JsonRpcHttpClientTransport(
41+
config != null ? config.getHttpClient() : fallback, agentInterface.url());
42+
}
43+
44+
@Override
45+
public String getTransportProtocol() {
46+
return TransportProtocol.JSONRPC.asString();
47+
}
48+
49+
@Override
50+
public Class<JsonRpcHttpClientTransport> getTransportProtocolClass() {
51+
return JsonRpcHttpClientTransport.class;
52+
}
53+
}

0 commit comments

Comments
 (0)