Skip to content

Commit cc7cafb

Browse files
committed
feat(a2a): migrate to new Client.builder API
Migrate A2AClientConsumer to use io.a2a.client.Client.builder with JSON-RPC transport, streaming event consumers, and CompletableFuture response handling. Replace reflection-based AgentCard access in UI panels with record getters. Update build.gradle and adjust Tool constructor in McpConfigService.
1 parent 82950d4 commit cc7cafb

File tree

5 files changed

+206
-66
lines changed

5 files changed

+206
-66
lines changed

build.gradle.kts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -429,6 +429,11 @@ project(":core") {
429429
implementation("io.ktor:ktor-server-sse:3.2.3")
430430

431431
implementation("io.github.a2asdk:a2a-java-sdk-client:0.3.0.Beta1")
432+
// A2A transport dependencies - JSON-RPC is included by default but we need to configure it
433+
// Add gRPC transport if needed in the future
434+
// implementation("io.github.a2asdk:a2a-java-sdk-client-transport-grpc:0.3.0.Beta1")
435+
// Add REST transport if needed in the future
436+
// implementation("io.github.a2asdk:a2a-java-sdk-client-transport-rest:0.3.0.Beta1")
432437

433438
implementation("io.reactivex.rxjava3:rxjava:3.1.10")
434439

core/src/main/kotlin/cc/unitmesh/devti/a2a/A2AClientConsumer.kt

Lines changed: 137 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,164 @@
11
package cc.unitmesh.devti.a2a
22

33
import io.a2a.A2A
4-
import io.a2a.client.A2AClient
4+
import io.a2a.client.Client
5+
import io.a2a.client.ClientEvent
6+
import io.a2a.client.MessageEvent
7+
import io.a2a.client.TaskEvent
8+
import io.a2a.client.TaskUpdateEvent
9+
import io.a2a.client.config.ClientConfig
10+
import io.a2a.client.transport.jsonrpc.JSONRPCTransport
11+
import io.a2a.client.transport.jsonrpc.JSONRPCTransportConfig
512
import io.a2a.spec.AgentCard
6-
import io.a2a.spec.MessageSendParams
7-
import io.a2a.spec.SendMessageResponse
13+
import io.a2a.spec.Message
14+
import io.a2a.client.http.A2ACardResolver
815
import kotlinx.serialization.Serializable
916
import com.fasterxml.jackson.databind.ObjectMapper
17+
import io.a2a.client.http.JdkA2AHttpClient
18+
import io.a2a.spec.Part
19+
import java.util.function.BiConsumer
20+
import java.util.function.Consumer
21+
import java.util.concurrent.CompletableFuture
22+
import java.util.concurrent.ConcurrentHashMap
1023

1124
class A2AClientConsumer {
12-
var clientMap: MutableMap<String, A2AClient> = mutableMapOf()
13-
var cardMap: MutableMap<String, AgentCard> = mutableMapOf()
14-
var jacksonObjectMapper = ObjectMapper()
15-
16-
fun init(servers: List<A2aServer>): List<A2AClient> {
17-
servers.forEach {
18-
val client = A2AClient(it.url)
19-
val cardName = client.agentCard.name
20-
cardMap[cardName] = client.getAgentCard()
21-
clientMap[cardName] = client
25+
private val clientMap: MutableMap<String, Client> = ConcurrentHashMap()
26+
private val cardMap: MutableMap<String, AgentCard> = ConcurrentHashMap()
27+
private val jacksonObjectMapper = ObjectMapper()
28+
private val responseMap: MutableMap<String, CompletableFuture<String>> = ConcurrentHashMap()
29+
30+
fun init(servers: List<A2aServer>): List<Client> {
31+
servers.forEach { server ->
32+
try {
33+
// Get agent card using A2ACardResolver
34+
val cardResolver = A2ACardResolver(server.url)
35+
val agentCard = cardResolver.getAgentCard()
36+
val agentName = agentCard.name()
37+
38+
// Create client configuration
39+
val clientConfig = ClientConfig.Builder()
40+
.setAcceptedOutputModes(listOf("text", "application/json"))
41+
.build()
42+
43+
// Create event consumers to handle responses
44+
val consumers = listOf<BiConsumer<ClientEvent, AgentCard>>(
45+
BiConsumer { event, card ->
46+
handleClientEvent(event, card, agentName)
47+
}
48+
)
49+
50+
// Create error handler
51+
val errorHandler = Consumer<Throwable> { error ->
52+
handleStreamingError(error, agentName)
53+
}
54+
55+
// Create the client using the builder pattern
56+
val client = Client.builder(agentCard)
57+
.clientConfig(clientConfig)
58+
.withTransport(JSONRPCTransport::class.java, JSONRPCTransportConfig())
59+
.addConsumers(consumers)
60+
.streamingErrorHandler(errorHandler)
61+
.build()
62+
63+
cardMap[agentName] = agentCard
64+
clientMap[agentName] = client
65+
} catch (e: Exception) {
66+
throw RuntimeException("Failed to initialize A2A client for ${server.url}: ${e.message}", e)
67+
}
2268
}
2369

2470
return clientMap.values.toList()
2571
}
2672

2773
fun listAgents(): List<AgentCard> {
28-
return clientMap.values.map { it.getAgentCard() }
74+
return cardMap.values.toList()
2975
}
3076

3177
fun sendMessage(agentName: String, msgText: String): String {
3278
val client = clientMap[agentName] ?: throw IllegalArgumentException("No client found for $agentName")
33-
val message = A2A.toUserMessage(msgText)
34-
val msgParams = MessageSendParams.Builder()
35-
.message(message)
36-
.build()
3779

3880
return try {
39-
val response: SendMessageResponse = client.sendMessage(msgParams)
40-
val resultString = jacksonObjectMapper.writeValueAsString(response.result)
41-
resultString
81+
// Create message using A2A utility
82+
val message = A2A.toUserMessage(msgText)
83+
84+
// Create a future to capture the response
85+
val responseFuture = CompletableFuture<String>()
86+
responseMap[agentName] = responseFuture
87+
88+
// Send the message using the new API
89+
client.sendMessage(message, null)
90+
91+
// Wait for response (with timeout)
92+
responseFuture.get(30, java.util.concurrent.TimeUnit.SECONDS)
4293
} catch (e: Exception) {
94+
responseMap.remove(agentName)
4395
throw RuntimeException("Failed to send message to agent $agentName: ${e.message}", e)
4496
}
4597
}
98+
99+
private fun handleClientEvent(event: ClientEvent, card: AgentCard, agentName: String) {
100+
when (event) {
101+
is MessageEvent -> {
102+
// Handle message response
103+
val message = event.message
104+
val responseFuture = responseMap[agentName]
105+
if (responseFuture != null && !responseFuture.isDone) {
106+
try {
107+
val responseText = extractTextFromMessage(message)
108+
responseFuture.complete(responseText)
109+
} catch (e: Exception) {
110+
responseFuture.completeExceptionally(e)
111+
} finally {
112+
responseMap.remove(agentName)
113+
}
114+
}
115+
}
116+
is TaskEvent -> {
117+
// Handle task events if needed
118+
// For now, we'll just log or ignore
119+
}
120+
is TaskUpdateEvent -> {
121+
// Handle task update events if needed
122+
// For now, we'll just log or ignore
123+
}
124+
}
125+
}
126+
127+
private fun handleStreamingError(error: Throwable, agentName: String) {
128+
val responseFuture = responseMap[agentName]
129+
if (responseFuture != null && !responseFuture.isDone) {
130+
responseFuture.completeExceptionally(error)
131+
responseMap.remove(agentName)
132+
}
133+
}
134+
135+
private fun extractTextFromMessage(message: Message): String {
136+
val parts = message.getParts()
137+
if (parts != null) {
138+
return parts.joinToString("") { part ->
139+
when {
140+
part.javaClass.simpleName == "TextPart" -> {
141+
// Try to use the text() method for TextPart record
142+
try {
143+
val textMethod = part.javaClass.getMethod("text")
144+
textMethod.invoke(part) as? String ?: ""
145+
} catch (e: Exception) {
146+
// Fallback to reflection for field access
147+
try {
148+
val textField = part.javaClass.getDeclaredField("text")
149+
textField.isAccessible = true
150+
textField.get(part) as? String ?: ""
151+
} catch (e2: Exception) {
152+
part.toString()
153+
}
154+
}
155+
}
156+
else -> part.toString()
157+
}
158+
}
159+
}
160+
return ""
161+
}
46162
}
47163

48164
@Serializable

core/src/main/kotlin/cc/unitmesh/devti/a2a/ui/A2AAgentCardPanel.kt

Lines changed: 58 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -24,39 +24,34 @@ class A2AAgentCardPanel(
2424
) : JPanel(BorderLayout(0, 0)) {
2525

2626
// Helper methods to safely access AgentCard properties
27-
// Use reflection to access Java record fields safely
27+
// Use the new AgentCard record methods
2828
private fun getAgentName(): String = try {
29-
getFieldValue(agentCard, "name") as? String ?: "Unknown Agent"
29+
agentCard.name() ?: "Unknown Agent"
3030
} catch (e: Exception) {
3131
"Unknown Agent"
3232
}
3333

3434
private fun getAgentDescription(): String = try {
35-
getFieldValue(agentCard, "description") as? String ?: "No description available"
35+
agentCard.description() ?: "No description available"
3636
} catch (e: Exception) {
3737
"No description available"
3838
}
3939

4040
private fun getAgentVersion(): String = try {
41-
getFieldValue(agentCard, "version") as? String ?: "1.0.0"
41+
agentCard.version() ?: "1.0.0"
4242
} catch (e: Exception) {
4343
"1.0.0"
4444
}
4545

4646
private fun getProviderName(): String = try {
47-
val provider = getFieldValue(agentCard, "provider")
48-
if (provider != null) {
49-
getFieldValue(provider, "name") as? String ?: "Unknown"
50-
} else {
51-
"Unknown"
52-
}
47+
val provider = agentCard.provider()
48+
provider?.organization() ?: "Unknown"
5349
} catch (e: Exception) {
5450
"Unknown"
5551
}
5652

5753
private fun getSkillsCount(): Int = try {
58-
val skills = getFieldValue(agentCard, "skills") as? List<*>
59-
skills?.size ?: 0
54+
agentCard.skills()?.size ?: 0
6055
} catch (e: Exception) {
6156
0
6257
}
@@ -198,7 +193,7 @@ class A2AAgentCardPanel(
198193
}
199194

200195
private fun getAgentName(): String = try {
201-
getFieldValue(agentCard, "name") as? String ?: "Unknown Agent"
196+
agentCard.name() ?: "Unknown Agent"
202197
} catch (e: Exception) {
203198
"Unknown Agent"
204199
}
@@ -228,32 +223,35 @@ class A2AAgentCardPanel(
228223

229224
// Basic Information
230225
addSectionHeader(panel, "Basic Information")
231-
addDetailRow(panel, "Name", getFieldValue(agentCard, "name") as? String ?: "N/A")
232-
addDetailRow(panel, "Description", getFieldValue(agentCard, "description") as? String ?: "N/A")
233-
addDetailRow(panel, "Version", getFieldValue(agentCard, "version") as? String ?: "N/A")
234-
addDetailRow(panel, "URL", getFieldValue(agentCard, "url") as? String ?: "N/A")
235-
addDetailRow(panel, "Protocol Version", getFieldValue(agentCard, "protocolVersion") as? String ?: "N/A")
226+
addDetailRow(panel, "Name", agentCard.name() ?: "N/A")
227+
addDetailRow(panel, "Description", agentCard.description() ?: "N/A")
228+
addDetailRow(panel, "Version", agentCard.version() ?: "N/A")
229+
addDetailRow(panel, "URL", agentCard.url() ?: "N/A")
230+
addDetailRow(panel, "Protocol Version", agentCard.protocolVersion() ?: "N/A")
236231

237232
// Provider Information
238-
val provider = getFieldValue(agentCard, "provider")
233+
val provider = agentCard.provider()
239234
if (provider != null) {
240235
addSectionHeader(panel, "Provider")
241-
addDetailRow(panel, "Name", getFieldValue(provider, "name") as? String ?: "N/A")
242-
addDetailRow(panel, "Description", getFieldValue(provider, "description") as? String ?: "N/A")
243-
addDetailRow(panel, "URL", getFieldValue(provider, "url") as? String ?: "N/A")
236+
try {
237+
addDetailRow(panel, "Organization", provider.organization() ?: "N/A")
238+
addDetailRow(panel, "URL", provider.url() ?: "N/A")
239+
} catch (e: Exception) {
240+
addDetailRow(panel, "Provider", provider.toString())
241+
}
244242
}
245243

246244
// Capabilities
247-
val capabilities = getFieldValue(agentCard, "capabilities")
245+
val capabilities = agentCard.capabilities()
248246
if (capabilities != null) {
249247
addSectionHeader(panel, "Capabilities")
250-
addDetailRow(panel, "Supports Streaming", getFieldValue(capabilities, "supportsStreaming")?.toString() ?: "N/A")
251-
addDetailRow(panel, "Supports Tools", getFieldValue(capabilities, "supportsTools")?.toString() ?: "N/A")
252-
addDetailRow(panel, "Supports Resources", getFieldValue(capabilities, "supportsResources")?.toString() ?: "N/A")
248+
addDetailRow(panel, "Supports Streaming", capabilities.streaming()?.toString() ?: "N/A")
249+
addDetailRow(panel, "Push Notifications", capabilities.pushNotifications()?.toString() ?: "N/A")
250+
addDetailRow(panel, "State Transition History", capabilities.stateTransitionHistory()?.toString() ?: "N/A")
253251
}
254252

255253
// Skills
256-
val skills = getFieldValue(agentCard, "skills") as? List<*>
254+
val skills = agentCard.skills()
257255
if (!skills.isNullOrEmpty()) {
258256
addSectionHeader(panel, "Skills (${skills.size})")
259257
skills.forEach { skill ->
@@ -265,17 +263,17 @@ class A2AAgentCardPanel(
265263

266264
// Input/Output Modes
267265
addSectionHeader(panel, "Input/Output Modes")
268-
val inputModes = getFieldValue(agentCard, "defaultInputModes") as? List<*>
269-
val outputModes = getFieldValue(agentCard, "defaultOutputModes") as? List<*>
266+
val inputModes = agentCard.defaultInputModes()
267+
val outputModes = agentCard.defaultOutputModes()
270268
addDetailRow(panel, "Default Input Modes", inputModes?.joinToString(", ") ?: "N/A")
271269
addDetailRow(panel, "Default Output Modes", outputModes?.joinToString(", ") ?: "N/A")
272270

273271
// Additional Information
274272
addSectionHeader(panel, "Additional Information")
275-
addDetailRow(panel, "Documentation URL", getFieldValue(agentCard, "documentationUrl") as? String ?: "N/A")
276-
addDetailRow(panel, "Icon URL", getFieldValue(agentCard, "iconUrl") as? String ?: "N/A")
277-
addDetailRow(panel, "Preferred Transport", getFieldValue(agentCard, "preferredTransport") as? String ?: "N/A")
278-
addDetailRow(panel, "Supports Auth Extended Card", getFieldValue(agentCard, "supportsAuthenticatedExtendedCard")?.toString() ?: "N/A")
273+
addDetailRow(panel, "Documentation URL", agentCard.documentationUrl() ?: "N/A")
274+
addDetailRow(panel, "Icon URL", agentCard.iconUrl() ?: "N/A")
275+
addDetailRow(panel, "Preferred Transport", agentCard.preferredTransport() ?: "N/A")
276+
addDetailRow(panel, "Supports Auth Extended Card", agentCard.supportsAuthenticatedExtendedCard()?.toString() ?: "N/A")
279277

280278
return panel
281279
}
@@ -320,8 +318,32 @@ class A2AAgentCardPanel(
320318
isOpaque = true
321319
}
322320

323-
val skillName = getFieldValue(skill, "name") as? String ?: "Unnamed Skill"
324-
val skillDesc = getFieldValue(skill, "description") as? String ?: "No description"
321+
// Try to access skill properties using the new API
322+
val skillName = try {
323+
when {
324+
skill.javaClass.simpleName == "AgentSkill" -> {
325+
// Use reflection for AgentSkill record methods
326+
val nameMethod = skill.javaClass.getMethod("name")
327+
nameMethod.invoke(skill) as? String ?: "Unnamed Skill"
328+
}
329+
else -> getFieldValue(skill, "name") as? String ?: "Unnamed Skill"
330+
}
331+
} catch (e: Exception) {
332+
"Unnamed Skill"
333+
}
334+
335+
val skillDesc = try {
336+
when {
337+
skill.javaClass.simpleName == "AgentSkill" -> {
338+
// Use reflection for AgentSkill record methods
339+
val descMethod = skill.javaClass.getMethod("description")
340+
descMethod.invoke(skill) as? String ?: "No description"
341+
}
342+
else -> getFieldValue(skill, "description") as? String ?: "No description"
343+
}
344+
} catch (e: Exception) {
345+
"No description"
346+
}
325347

326348
val nameLabel = JBLabel("$skillName").apply {
327349
font = JBUI.Fonts.label(12.0f).asBold()
@@ -358,7 +380,7 @@ class A2AAgentCardPanel(
358380
}
359381

360382
private fun getAgentName(): String = try {
361-
getFieldValue(agentCard, "name") as? String ?: "Unknown Agent"
383+
agentCard.name() ?: "Unknown Agent"
362384
} catch (e: Exception) {
363385
"Unknown Agent"
364386
}

0 commit comments

Comments
 (0)