Skip to content

Commit 97e63fa

Browse files
committed
feat: Upgrade to a2a-java 1.0.0.Alpha3
Also add tests for the a2a-java samples. Add a workflow to run above tests.
1 parent f140c6d commit 97e63fa

39 files changed

Lines changed: 1451 additions & 202 deletions

File tree

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
name: Build and Test Java Agent Samples
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
paths:
8+
- 'samples/java/agents/**'
9+
pull_request:
10+
paths:
11+
- 'samples/java/agents/**'
12+
13+
jobs:
14+
build-and-test:
15+
name: Build and Test Java Agents
16+
runs-on: ubuntu-latest
17+
permissions:
18+
contents: read
19+
20+
steps:
21+
- name: Checkout repository
22+
uses: actions/checkout@v5
23+
24+
- name: Set up Java 17
25+
uses: actions/setup-java@v4
26+
with:
27+
distribution: 'temurin'
28+
java-version: '17'
29+
cache: 'maven'
30+
31+
- name: Set up JBang
32+
uses: jbangdev/setup-jbang@main
33+
34+
- name: Build and test Java agent samples
35+
working-directory: samples/java/agents
36+
run: mvn -B clean install
37+
38+
- name: Upload test results
39+
if: always()
40+
uses: actions/upload-artifact@v4
41+
with:
42+
name: test-results
43+
path: samples/java/agents/**/target/surefire-reports/
44+
retention-days: 7

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,9 @@ ENV/
117117
env.bak/
118118
venv.bak/
119119

120+
# UV lock files auto-generated during Java agent tests
121+
samples/java/agents/**/mcp/uv.lock
122+
120123
# Spyder project settings
121124
.spyderproject
122125
.spyproject

samples/java/agents/content_editor/pom.xml

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,22 +16,53 @@
1616
<dependency>
1717
<groupId>io.github.a2asdk</groupId>
1818
<artifactId>a2a-java-sdk-reference-jsonrpc</artifactId>
19-
<version>${io.a2a.sdk.version}</version>
2019
</dependency>
2120
<dependency>
2221
<groupId>io.quarkus</groupId>
23-
<artifactId>quarkus-rest-jackson</artifactId>
22+
<artifactId>quarkus-rest</artifactId>
2423
</dependency>
2524
<dependency>
2625
<groupId>jakarta.enterprise</groupId>
2726
<artifactId>jakarta.enterprise.cdi-api</artifactId>
28-
<version>${jakarta.enterprise.cdi-api.version}</version>
2927
</dependency>
3028
<dependency>
3129
<groupId>io.quarkiverse.langchain4j</groupId>
3230
<artifactId>quarkus-langchain4j-ai-gemini</artifactId>
3331
<version>${quarkus.langchain4j.version}</version>
3432
</dependency>
33+
34+
<!-- Test dependencies -->
35+
<dependency>
36+
<groupId>io.quarkus</groupId>
37+
<artifactId>quarkus-junit5</artifactId>
38+
<scope>test</scope>
39+
</dependency>
40+
<dependency>
41+
<groupId>io.rest-assured</groupId>
42+
<artifactId>rest-assured</artifactId>
43+
<scope>test</scope>
44+
</dependency>
45+
<dependency>
46+
<groupId>org.wiremock</groupId>
47+
<artifactId>wiremock</artifactId>
48+
<scope>test</scope>
49+
</dependency>
50+
<dependency>
51+
<groupId>com.samples.a2a</groupId>
52+
<artifactId>test-utils</artifactId>
53+
<version>${project.version}</version>
54+
<scope>test</scope>
55+
</dependency>
56+
<dependency>
57+
<groupId>io.github.a2asdk</groupId>
58+
<artifactId>a2a-java-sdk-client</artifactId>
59+
<scope>test</scope>
60+
</dependency>
61+
<dependency>
62+
<groupId>io.github.a2asdk</groupId>
63+
<artifactId>a2a-java-sdk-client-transport-jsonrpc</artifactId>
64+
<scope>test</scope>
65+
</dependency>
3566
</dependencies>
3667

3768
<build>

samples/java/agents/content_editor/src/main/java/com/samples/a2a/ContentEditorAgentCardProducer.java

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import io.a2a.server.PublicAgentCard;
44
import io.a2a.spec.AgentCapabilities;
55
import io.a2a.spec.AgentCard;
6+
import io.a2a.spec.AgentInterface;
67
import io.a2a.spec.AgentSkill;
78
import jakarta.enterprise.context.ApplicationScoped;
89
import jakarta.enterprise.inject.Produces;
@@ -40,23 +41,23 @@ public int getHttpPort() {
4041
@Produces
4142
@PublicAgentCard
4243
public AgentCard agentCard() {
43-
return new AgentCard.Builder()
44+
return AgentCard.builder()
4445
.name("Content Editor Agent")
4546
.description("An agent that can proof-read and polish content.")
46-
.url("http://localhost:" + getHttpPort())
47+
.supportedInterfaces(Collections.singletonList(
48+
new AgentInterface("JSONRPC", "http://localhost:" + getHttpPort())))
4749
.version("1.0.0")
4850
.documentationUrl("http://example.com/docs")
4951
.capabilities(
50-
new AgentCapabilities.Builder()
52+
AgentCapabilities.builder()
5153
.streaming(true)
5254
.pushNotifications(false)
53-
.stateTransitionHistory(false)
5455
.build())
5556
.defaultInputModes(Collections.singletonList("text"))
5657
.defaultOutputModes(Collections.singletonList("text"))
5758
.skills(
5859
Collections.singletonList(
59-
new AgentSkill.Builder()
60+
AgentSkill.builder()
6061
.id("editor")
6162
.name("Edits content")
6263
.description("Edits content by proof-reading and polishing")
@@ -66,7 +67,6 @@ public AgentCard agentCard() {
6667
"Edit the following article, make sure it has "
6768
+ "a professional tone"))
6869
.build()))
69-
.protocolVersion("0.3.0")
7070
.build();
7171
}
7272
}

samples/java/agents/content_editor/src/main/java/com/samples/a2a/ContentEditorAgentExecutorProducer.java

Lines changed: 15 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,8 @@
88

99
import io.a2a.server.agentexecution.AgentExecutor;
1010
import io.a2a.server.agentexecution.RequestContext;
11-
import io.a2a.server.events.EventQueue;
12-
import io.a2a.server.tasks.TaskUpdater;
13-
import io.a2a.spec.JSONRPCError;
11+
import io.a2a.server.tasks.AgentEmitter;
12+
import io.a2a.spec.A2AError;
1413
import io.a2a.spec.Message;
1514
import io.a2a.spec.Part;
1615
import io.a2a.spec.Task;
@@ -72,14 +71,12 @@ private static class ContentEditorAgentExecutor implements AgentExecutor {
7271

7372
@Override
7473
public void execute(final RequestContext context,
75-
final EventQueue eventQueue) throws JSONRPCError {
76-
final TaskUpdater updater = new TaskUpdater(context, eventQueue);
77-
74+
final AgentEmitter emitter) throws A2AError {
7875
// mark the task as submitted and start working on it
7976
if (context.getTask() == null) {
80-
updater.submit();
77+
emitter.submit();
8178
}
82-
updater.startWork();
79+
emitter.startWork();
8380

8481
// extract the text from the message
8582
final String assignment = extractTextFromMessage(
@@ -89,20 +86,20 @@ public void execute(final RequestContext context,
8986
final String response = agent.editContent(assignment);
9087

9188
// create the response part
92-
final TextPart responsePart = new TextPart(response, null);
89+
final TextPart responsePart = new TextPart(response);
9390
final List<Part<?>> parts = List.of(responsePart);
9491

9592
// add the response as an artifact and complete the task
96-
updater.addArtifact(parts, null, null, null);
97-
updater.complete();
93+
emitter.addArtifact(parts);
94+
emitter.complete();
9895
}
9996

10097
private String extractTextFromMessage(final Message message) {
10198
final StringBuilder textBuilder = new StringBuilder();
102-
if (message.getParts() != null) {
103-
for (final Part part : message.getParts()) {
99+
if (message.parts() != null) {
100+
for (final Part part : message.parts()) {
104101
if (part instanceof TextPart textPart) {
105-
textBuilder.append(textPart.getText());
102+
textBuilder.append(textPart.text());
106103
}
107104
}
108105
}
@@ -111,22 +108,21 @@ private String extractTextFromMessage(final Message message) {
111108

112109
@Override
113110
public void cancel(final RequestContext context,
114-
final EventQueue eventQueue) throws JSONRPCError {
111+
final AgentEmitter emitter) throws A2AError {
115112
final Task task = context.getTask();
116113

117-
if (task.getStatus().state() == TaskState.CANCELED) {
114+
if (task.status().state() == TaskState.TASK_STATE_CANCELED) {
118115
// task already cancelled
119116
throw new TaskNotCancelableError();
120117
}
121118

122-
if (task.getStatus().state() == TaskState.COMPLETED) {
119+
if (task.status().state() == TaskState.TASK_STATE_COMPLETED) {
123120
// task already completed
124121
throw new TaskNotCancelableError();
125122
}
126123

127124
// cancel the task
128-
final TaskUpdater updater = new TaskUpdater(context, eventQueue);
129-
updater.cancel();
125+
emitter.cancel();
130126
}
131127
}
132128
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
package com.samples.a2a;
2+
3+
import com.github.tomakehurst.wiremock.WireMockServer;
4+
import com.samples.a2a.test.A2AClientTestUtils;
5+
import com.samples.a2a.test.GeminiMockServer;
6+
import io.a2a.A2A;
7+
import io.a2a.client.Client;
8+
import io.a2a.client.ClientEvent;
9+
import io.a2a.client.MessageEvent;
10+
import io.a2a.client.TaskUpdateEvent;
11+
import io.a2a.client.config.ClientConfig;
12+
import io.a2a.client.http.A2ACardResolver;
13+
import io.a2a.client.transport.jsonrpc.JSONRPCTransport;
14+
import io.a2a.client.transport.jsonrpc.JSONRPCTransportConfig;
15+
import io.a2a.spec.AgentCard;
16+
import io.a2a.spec.Artifact;
17+
import io.a2a.spec.Message;
18+
import io.a2a.spec.Part;
19+
import io.a2a.spec.TaskArtifactUpdateEvent;
20+
import io.a2a.spec.TaskStatusUpdateEvent;
21+
import io.a2a.spec.TextPart;
22+
import io.a2a.spec.UpdateEvent;
23+
import io.quarkus.test.junit.QuarkusTest;
24+
import org.eclipse.microprofile.config.inject.ConfigProperty;
25+
import org.junit.jupiter.api.*;
26+
27+
import java.util.ArrayList;
28+
import java.util.List;
29+
import java.util.concurrent.CompletableFuture;
30+
import java.util.concurrent.TimeUnit;
31+
import java.util.function.BiConsumer;
32+
import java.util.function.Consumer;
33+
34+
import static org.junit.jupiter.api.Assertions.*;
35+
36+
/**
37+
* Integration test using A2A Client with streaming approach.
38+
* Tests the full A2A protocol stack: Client -> AgentExecutor -> AI Service.
39+
*/
40+
@QuarkusTest
41+
public class ContentEditorA2AIntegrationTest {
42+
private static WireMockServer wireMockServer;
43+
44+
@ConfigProperty(name = "quarkus.http.test-port")
45+
int serverPort;
46+
47+
@BeforeAll
48+
public static void setupWireMock() {
49+
wireMockServer = new WireMockServer(8089);
50+
wireMockServer.start();
51+
}
52+
53+
@AfterEach
54+
public void resetWireMock() {
55+
wireMockServer.resetAll();
56+
}
57+
58+
@AfterAll
59+
public static void teardownWireMock() {
60+
if (wireMockServer != null) {
61+
wireMockServer.stop();
62+
}
63+
}
64+
65+
@Test
66+
public void testContentEditorViaA2AProtocol() throws Exception {
67+
// Mock Gemini response
68+
GeminiMockServer.mockTextResponse(wireMockServer, "Content has been polished!");
69+
70+
// Fetch agent card
71+
String serverUrl = "http://localhost:" + serverPort;
72+
AgentCard agentCard = new A2ACardResolver(serverUrl).getAgentCard();
73+
74+
// Create CompletableFuture to capture async response
75+
CompletableFuture<String> messageResponse = new CompletableFuture<>();
76+
77+
// Create event consumers
78+
List<BiConsumer<ClientEvent, AgentCard>> consumers = A2AClientTestUtils.createEventConsumers(messageResponse);
79+
80+
// Create error handler
81+
Consumer<Throwable> errorHandler = error -> {
82+
messageResponse.completeExceptionally(error);
83+
};
84+
85+
// Create streaming client
86+
Client client = Client.builder(agentCard)
87+
.addConsumers(consumers)
88+
.streamingErrorHandler(errorHandler)
89+
.withTransport(JSONRPCTransport.class, new JSONRPCTransportConfig())
90+
.clientConfig(new ClientConfig.Builder().build())
91+
.build();
92+
93+
// Send message
94+
Message message = A2A.toUserMessage("Please edit this content");
95+
client.sendMessage(message);
96+
97+
// Wait for response with timeout
98+
String response = messageResponse.get(10, TimeUnit.SECONDS);
99+
100+
assertNotNull(response, "Should have response");
101+
assertTrue(response.length() > 0, "Response should not be empty");
102+
}
103+
104+
}
105+
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Test configuration - point Gemini to WireMock server
2+
quarkus.langchain4j.ai.gemini.base-url=http://localhost:${wiremock.server.port:8089}
3+
quarkus.langchain4j.ai.gemini.api-key=test-key
4+
quarkus.langchain4j.ai.gemini.chat-model.model-id=gemini-2.5-flash
5+
quarkus.langchain4j.ai.gemini.timeout=5000
6+
7+
# Use different port for test server
8+
quarkus.http.test-port=11001
9+
quarkus.http.port=11001
10+
quarkus.grpc.server.use-separate-server=false

0 commit comments

Comments
 (0)