Skip to content

Commit 56b09b2

Browse files
committed
v0.9 streaming parser suite
Implements the full incremental A2UI v0.9 streaming parser suite in Kotlin, achieving SDK parity. Automatically incorporates critical subsequent fixes for robust real-time topology parsing and relative bindings. Port of Python SDK commit 8ba982a
1 parent e538f30 commit 56b09b2

15 files changed

Lines changed: 2597 additions & 363 deletions

File tree

agent_sdks/kotlin/src/main/kotlin/com/google/a2ui/a2a/A2aHandler.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package com.google.a2ui.a2a
1818

1919
import com.google.a2ui.core.parser.hasA2uiParts
2020
import com.google.a2ui.core.parser.parseResponseToParts
21+
import com.google.a2ui.core.schema.A2uiConstants
2122
import com.google.adk.agents.RunConfig
2223
import com.google.adk.events.Event
2324
import com.google.adk.runner.Runner
@@ -41,6 +42,7 @@ class A2aHandler(private val runner: Runner) {
4142
agentName: String,
4243
serverUrl: String,
4344
supportedCatalogIds: List<String> = emptyList(),
45+
version: String = A2uiConstants.VERSION_0_9,
4446
): Map<String, Any> {
4547
return mapOf(
4648
"name" to agentName,
@@ -52,7 +54,7 @@ class A2aHandler(private val runner: Runner) {
5254
"extensions" to
5355
listOf(
5456
mapOf(
55-
"uri" to A2uiA2a.A2UI_EXTENSION_URI,
57+
"uri" to "${A2uiA2a.A2UI_EXTENSION_BASE_URI}$version",
5658
"params" to mapOf("supportedCatalogIds" to supportedCatalogIds),
5759
)
5860
),

agent_sdks/kotlin/src/main/kotlin/com/google/a2ui/a2a/A2uiA2a.kt

Lines changed: 54 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import kotlinx.serialization.json.JsonElement
2424

2525
/** A2A protocol helpers for A2UI integration. */
2626
object A2uiA2a {
27-
const val A2UI_EXTENSION_URI = "https://a2ui.org/a2a-extension/a2ui/v0.8"
27+
const val A2UI_EXTENSION_BASE_URI = "https://a2ui.org/a2a-extension/a2ui/v"
2828
const val MIME_TYPE_KEY = "mimeType"
2929
const val A2UI_MIME_TYPE = "application/json+a2ui"
3030

@@ -42,12 +42,13 @@ object A2uiA2a {
4242

4343
/** Creates the A2UI AgentExtension configuration. */
4444
fun getA2uiAgentExtension(
45+
version: String,
4546
acceptsInlineCatalogs: Boolean = false,
4647
supportedCatalogIds: List<String> = emptyList(),
4748
): AgentExtension {
4849
val params = mutableMapOf<String, Any>()
4950
if (acceptsInlineCatalogs) {
50-
params[A2uiConstants.INLINE_CATALOGS_KEY] = true
51+
params[A2uiConstants.ACCEPTS_INLINE_CATALOGS_KEY] = true
5152
}
5253
if (supportedCatalogIds.isNotEmpty()) {
5354
params[A2uiConstants.SUPPORTED_CATALOG_IDS_KEY] = supportedCatalogIds
@@ -56,27 +57,70 @@ object A2uiA2a {
5657
val isSupportRequired = false
5758
return AgentExtension(
5859
"Provides agent driven UI using the A2UI JSON format.",
59-
params,
60+
if (params.isEmpty()) null else params,
6061
isSupportRequired,
61-
A2UI_EXTENSION_URI,
62+
"$A2UI_EXTENSION_BASE_URI$version",
6263
)
6364
}
6465

66+
/**
67+
* Selects the newest A2UI extension URI from the matched extensions.
68+
*
69+
* @param requestedExtensions List of extension URIs requested by the client.
70+
* @param advertisedExtensions List of extension URIs advertised by the agent.
71+
* @return The newest overlapping A2UI extension URI, or null if none match.
72+
*/
73+
fun selectNewestA2uiExtension(
74+
requestedExtensions: List<String>,
75+
advertisedExtensions: List<String>,
76+
): String? {
77+
val baseUri = A2UI_EXTENSION_BASE_URI
78+
val matched =
79+
requestedExtensions.intersect(advertisedExtensions.toSet()).filter { it.startsWith(baseUri) }
80+
81+
if (matched.isEmpty()) return null
82+
83+
return matched.maxWithOrNull(
84+
Comparator { uri1, uri2 ->
85+
val v1 = uri1.removePrefix(baseUri)
86+
val v2 = uri2.removePrefix(baseUri)
87+
compareVersions(v1, v2)
88+
}
89+
)
90+
}
91+
92+
private fun compareVersions(v1: String, v2: String): Int {
93+
val parts1 = v1.split('.').map { it.toIntOrNull() ?: 0 }
94+
val parts2 = v2.split('.').map { it.toIntOrNull() ?: 0 }
95+
val length = maxOf(parts1.size, parts2.size)
96+
for (i in 0 until length) {
97+
val p1 = parts1.getOrElse(i) { 0 }
98+
val p2 = parts2.getOrElse(i) { 0 }
99+
if (p1 != p2) {
100+
return p1.compareTo(p2)
101+
}
102+
}
103+
return 0
104+
}
105+
65106
/**
66107
* Activates the A2UI extension if requested in the context.
67108
*
68109
* @param requestedExtensions List of extension URIs requested by the client.
110+
* @param advertisedExtensions List of extension URIs advertised by the agent.
69111
* @param addActivatedExtension Callback to register an activated extension.
70-
* @return True if A2UI was activated, false otherwise.
112+
* @return The version string of the activated A2UI extension, or null if not activated.
71113
*/
72114
fun tryActivateA2uiExtension(
73115
requestedExtensions: List<String>,
116+
advertisedExtensions: List<String>,
74117
addActivatedExtension: (String) -> Unit,
75-
): Boolean {
76-
if (A2UI_EXTENSION_URI in requestedExtensions) {
77-
addActivatedExtension(A2UI_EXTENSION_URI)
78-
return true
118+
): String? {
119+
val selectedUri = selectNewestA2uiExtension(requestedExtensions, advertisedExtensions)
120+
if (selectedUri != null) {
121+
addActivatedExtension(selectedUri)
122+
return selectedUri.removePrefix(A2UI_EXTENSION_BASE_URI)
79123
}
80-
return false
124+
return null
81125
}
82126
}

0 commit comments

Comments
 (0)