Skip to main content

adk_ui/
protocol_capabilities.rs

1use serde::Serialize;
2
3/// Default runtime protocol profile for server integrations.
4///
5/// New integrations should prefer an explicit protocol (`a2ui`, `ag_ui`,
6/// `mcp_apps`, or `awp`). The legacy default remains for compatibility until
7/// the 2026-12-31 sunset.
8pub const UI_DEFAULT_PROTOCOL: &str = "adk_ui";
9
10/// Preferred default for new integrations (A2UI current production family).
11pub const UI_PREFERRED_PROTOCOL: &str = "a2ui";
12
13/// Tool envelope version used by protocol-aware legacy tool responses.
14pub const TOOL_ENVELOPE_VERSION: &str = "1.0";
15
16/// Version of the machine-readable capability document exposed by examples.
17pub const UI_CAPABILITY_SCHEMA_VERSION: &str = "2026-08-13";
18
19/// Supported runtime protocol profile values.
20pub const SUPPORTED_UI_PROTOCOLS: &[&str] = &["adk_ui", "a2ui", "ag_ui", "mcp_apps"];
21
22/// Planned deprecation metadata for runtime/profile consumers.
23#[derive(Debug, Clone, Serialize)]
24#[serde(rename_all = "camelCase")]
25pub struct UiProtocolDeprecationSpec {
26    pub stage: &'static str,
27    pub announced_on: &'static str,
28    #[serde(skip_serializing_if = "Option::is_none")]
29    pub sunset_target_on: Option<&'static str>,
30    pub replacement_protocols: &'static [&'static str],
31    #[serde(skip_serializing_if = "Option::is_none")]
32    pub note: Option<&'static str>,
33}
34
35/// Implementation maturity for protocol support exposed to clients.
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
37#[serde(rename_all = "snake_case")]
38pub enum UiProtocolImplementationTier {
39    Legacy,
40    NativeSubset,
41    HybridSubset,
42    CompatibilitySubset,
43}
44
45/// Upstream specification track referenced by the runtime capability signal.
46#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
47#[serde(rename_all = "snake_case")]
48pub enum UiProtocolSpecTrack {
49    Internal,
50    Stable,
51    Draft,
52    Candidate,
53}
54
55/// Truthful implementation status for an individual protocol capability.
56#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
57#[serde(rename_all = "snake_case")]
58pub enum UiCapabilityStatus {
59    Supported,
60    Partial,
61    HostOwned,
62    Candidate,
63    Planned,
64    Legacy,
65}
66
67/// Layer responsible for an individual capability claim.
68#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
69#[serde(rename_all = "snake_case")]
70pub enum UiCapabilityScope {
71    Runtime,
72    Client,
73    Transport,
74    Host,
75    Security,
76}
77
78/// Maturity and implementation status for one upstream protocol version.
79#[derive(Debug, Clone, Serialize)]
80#[serde(rename_all = "camelCase")]
81pub struct UiProtocolVersionSpec {
82    pub version: &'static str,
83    pub spec_track: UiProtocolSpecTrack,
84    pub status: UiCapabilityStatus,
85    pub specification_url: &'static str,
86}
87
88/// How clients and servers discover or negotiate a protocol profile.
89#[derive(Debug, Clone, Serialize)]
90#[serde(rename_all = "camelCase")]
91pub struct UiProtocolNegotiationSpec {
92    pub mode: &'static str,
93    pub capability_keys: &'static [&'static str],
94    pub content_types: &'static [&'static str],
95    pub transports: &'static [&'static str],
96    pub fallback_protocols: &'static [&'static str],
97}
98
99/// Machine-readable, scoped capability claim. Unlike `features`, this makes
100/// partial and host-owned behavior explicit so clients can adapt safely.
101#[derive(Debug, Clone, Serialize)]
102#[serde(rename_all = "camelCase")]
103pub struct UiProtocolCapabilityClaim {
104    pub id: &'static str,
105    pub status: UiCapabilityStatus,
106    pub scope: UiCapabilityScope,
107    #[serde(skip_serializing_if = "Option::is_none")]
108    pub note: Option<&'static str>,
109}
110
111/// Static capability contract for each supported UI protocol.
112#[derive(Debug, Clone, Serialize)]
113#[serde(rename_all = "camelCase")]
114pub struct UiProtocolCapabilitySpec {
115    pub protocol: &'static str,
116    pub versions: &'static [&'static str],
117    pub implementation_tier: UiProtocolImplementationTier,
118    pub spec_track: UiProtocolSpecTrack,
119    pub reviewed_on: &'static str,
120    pub version_details: &'static [UiProtocolVersionSpec],
121    pub negotiation: UiProtocolNegotiationSpec,
122    pub capability_claims: &'static [UiProtocolCapabilityClaim],
123    pub summary: &'static str,
124    pub features: &'static [&'static str],
125    pub limitations: &'static [&'static str],
126    #[serde(skip_serializing_if = "Option::is_none")]
127    pub deprecation: Option<&'static UiProtocolDeprecationSpec>,
128}
129
130pub const ADK_UI_LEGACY_DEPRECATION: UiProtocolDeprecationSpec = UiProtocolDeprecationSpec {
131    stage: "planned",
132    announced_on: "2026-02-07",
133    sunset_target_on: Some("2026-12-31"),
134    replacement_protocols: &["a2ui", "ag_ui", "mcp_apps"],
135    note: Some(
136        "Legacy adk_ui profile remains supported during migration. Prefer a2ui for new integrations.",
137    ),
138};
139
140pub const UI_PROTOCOL_CAPABILITIES: &[UiProtocolCapabilitySpec] = &[
141    UiProtocolCapabilitySpec {
142        protocol: "adk_ui",
143        versions: &["1.0"],
144        implementation_tier: UiProtocolImplementationTier::Legacy,
145        spec_track: UiProtocolSpecTrack::Internal,
146        reviewed_on: UI_CAPABILITY_SCHEMA_VERSION,
147        version_details: &[UiProtocolVersionSpec {
148            version: "1.0",
149            spec_track: UiProtocolSpecTrack::Internal,
150            status: UiCapabilityStatus::Legacy,
151            specification_url: "https://github.com/zavora-ai/adk-ui",
152        }],
153        negotiation: UiProtocolNegotiationSpec {
154            mode: "profile_selection",
155            capability_keys: &["x-adk-ui-protocol", "uiProtocol"],
156            content_types: &["application/json", "text/event-stream"],
157            transports: &["http", "sse"],
158            fallback_protocols: &[],
159        },
160        capability_claims: &[
161            UiProtocolCapabilityClaim {
162                id: "legacy_surface_runtime",
163                status: UiCapabilityStatus::Legacy,
164                scope: UiCapabilityScope::Runtime,
165                note: Some("Retained only for compatibility through the published sunset window."),
166            },
167            UiProtocolCapabilityClaim {
168                id: "trusted_component_catalog",
169                status: UiCapabilityStatus::Supported,
170                scope: UiCapabilityScope::Security,
171                note: None,
172            },
173        ],
174        summary: "Legacy internal runtime profile retained for backward compatibility during migration.",
175        features: &["legacy_components", "theme", "events"],
176        limitations: &[
177            "Deprecated for new integrations.",
178            "Does not represent a standard external protocol surface.",
179        ],
180        deprecation: Some(&ADK_UI_LEGACY_DEPRECATION),
181    },
182    UiProtocolCapabilitySpec {
183        protocol: "a2ui",
184        versions: &["0.9.1", "1.0"],
185        implementation_tier: UiProtocolImplementationTier::NativeSubset,
186        spec_track: UiProtocolSpecTrack::Stable,
187        reviewed_on: UI_CAPABILITY_SCHEMA_VERSION,
188        version_details: &[
189            UiProtocolVersionSpec {
190                version: "0.9.1",
191                spec_track: UiProtocolSpecTrack::Stable,
192                status: UiCapabilityStatus::Supported,
193                specification_url: "https://a2ui.org/specification/v0.9.1-a2ui/",
194            },
195            UiProtocolVersionSpec {
196                version: "1.0",
197                spec_track: UiProtocolSpecTrack::Candidate,
198                status: UiCapabilityStatus::Candidate,
199                specification_url: "https://a2ui.org/specification/v1.0-a2ui/",
200            },
201        ],
202        negotiation: UiProtocolNegotiationSpec {
203            mode: "catalog_capability_exchange",
204            capability_keys: &[
205                "a2uiClientCapabilities.v0.9",
206                "a2uiRendererCapabilities.v1.0",
207            ],
208            content_types: &["application/a2ui+json", "application/json+a2ui"],
209            transports: &["jsonl", "sse", "a2a_host_binding"],
210            fallback_protocols: &["adk_ui"],
211        },
212        capability_claims: &[
213            UiProtocolCapabilityClaim {
214                id: "surface_lifecycle",
215                status: UiCapabilityStatus::Supported,
216                scope: UiCapabilityScope::Runtime,
217                note: Some("create, update components/data, and delete are supported."),
218            },
219            UiProtocolCapabilityClaim {
220                id: "catalog_negotiation",
221                status: UiCapabilityStatus::Partial,
222                scope: UiCapabilityScope::Transport,
223                note: Some(
224                    "Versioned capability shapes are emitted; transport-specific selection remains host-owned.",
225                ),
226            },
227            UiProtocolCapabilityClaim {
228                id: "v1_correlated_rpc",
229                status: UiCapabilityStatus::Candidate,
230                scope: UiCapabilityScope::Client,
231                note: Some(
232                    "actionResponse, callFunction, and functionResponse are implemented against the v1.0 release candidate.",
233                ),
234            },
235            UiProtocolCapabilityClaim {
236                id: "declarative_security",
237                status: UiCapabilityStatus::Supported,
238                scope: UiCapabilityScope::Security,
239                note: Some(
240                    "Generated payloads are constrained to trusted component and function catalogs.",
241                ),
242            },
243            UiProtocolCapabilityClaim {
244                id: "a2a_transport_binding",
245                status: UiCapabilityStatus::HostOwned,
246                scope: UiCapabilityScope::Transport,
247                note: None,
248            },
249        ],
250        summary: "A2UI v0.9.1 production surfaces plus an explicitly candidate v1.0 RPC subset, with versioned catalog capability exchange and declarative rendering.",
251        features: &[
252            "jsonl",
253            "versioned_envelopes_v0_9_1",
254            "versioned_envelopes_v1_0",
255            "flat_components",
256            "createSurface",
257            "updateComponents",
258            "updateDataModel",
259            "deleteSurface",
260            "client_action_messages",
261            "action_want_response",
262            "action_response",
263            "call_function",
264            "function_response",
265            "validation_failed_errors",
266            "client_metadata",
267            "validation_feedback",
268            "basic_catalog_functions",
269            "local_actions",
270            "mime_application_a2ui_json",
271            "optional_bulk_createSurface_fields",
272        ],
273        limitations: &[
274            "A2UI v1.0 multi-catalog mix and full catalog composition constraints are partial; actionResponse/callFunction are implemented end-to-end.",
275            "Dedicated A2A transport bindings remain host-owned; this crate emits protocol JSONL and client action envelopes.",
276        ],
277        deprecation: None,
278    },
279    UiProtocolCapabilitySpec {
280        protocol: "ag_ui",
281        versions: &["0.1"],
282        implementation_tier: UiProtocolImplementationTier::NativeSubset,
283        spec_track: UiProtocolSpecTrack::Stable,
284        reviewed_on: UI_CAPABILITY_SCHEMA_VERSION,
285        version_details: &[UiProtocolVersionSpec {
286            version: "0.1",
287            spec_track: UiProtocolSpecTrack::Stable,
288            status: UiCapabilityStatus::Supported,
289            specification_url: "https://docs.ag-ui.com/",
290        }],
291        negotiation: UiProtocolNegotiationSpec {
292            mode: "dynamic_discovery",
293            capability_keys: &["getCapabilities", "agentCapabilities"],
294            content_types: &["text/event-stream", "application/json"],
295            transports: &["sse", "http"],
296            fallback_protocols: &["adk_ui"],
297        },
298        capability_claims: &[
299            UiProtocolCapabilityClaim {
300                id: "typed_agent_capabilities",
301                status: UiCapabilityStatus::Partial,
302                scope: UiCapabilityScope::Runtime,
303                note: Some(
304                    "The example emits a truthful server baseline; loaded-agent tool, modality, sub-agent, and limit derivation remains framework-owned.",
305                ),
306            },
307            UiProtocolCapabilityClaim {
308                id: "event_stream",
309                status: UiCapabilityStatus::Supported,
310                scope: UiCapabilityScope::Transport,
311                note: None,
312            },
313            UiProtocolCapabilityClaim {
314                id: "state_and_activity_sync",
315                status: UiCapabilityStatus::Supported,
316                scope: UiCapabilityScope::Client,
317                note: None,
318            },
319            UiProtocolCapabilityClaim {
320                id: "encrypted_reasoning_continuity",
321                status: UiCapabilityStatus::Supported,
322                scope: UiCapabilityScope::Client,
323                note: Some(
324                    "Opaque REASONING_ENCRYPTED_VALUE events are preserved without exposing chain-of-thought.",
325                ),
326            },
327            UiProtocolCapabilityClaim {
328                id: "interrupt_resume",
329                status: UiCapabilityStatus::Partial,
330                scope: UiCapabilityScope::Runtime,
331                note: Some(
332                    "Wire types and resume ingestion are supported; durable pause/checkpoint policy is agent-owned.",
333                ),
334            },
335            UiProtocolCapabilityClaim {
336                id: "resumable_stream_sequence",
337                status: UiCapabilityStatus::Planned,
338                scope: UiCapabilityScope::Transport,
339                note: None,
340            },
341        ],
342        summary: "AG-UI event streaming with a typed capability model, state/activity synchronization, encrypted reasoning continuity, and interrupt-aware wire contracts.",
343        features: &[
344            "event_stream",
345            "run_lifecycle",
346            "run_started_parent_run_id",
347            "native_run_input_ingest",
348            "stable_text_events",
349            "text_message_chunk",
350            "stable_tool_events",
351            "tool_call_chunk",
352            "tool_call_result",
353            "state_snapshot",
354            "state_delta",
355            "messages_snapshot",
356            "activity_snapshot",
357            "activity_delta",
358            "reasoning_events",
359            "reasoning_message_chunk",
360            "reasoning_encrypted_value",
361            "agent_capability_discovery",
362            "interrupt_resume_wire_types",
363            "custom_surface_event",
364            "push_notification_channel",
365            "surface_activity_snapshot",
366            "run_error",
367            "raw_events",
368        ],
369        limitations: &[
370            "Example hosts may still wrap some ADK runtime events; prefer protocol_native SSE when available from adk-server.",
371            "Durable checkpointing and approval policy for interrupt/resume remain application-defined.",
372            "WebSocket, binary HTTP, and resumable sequence transports are not implemented by the example server; hosts must bridge the agent notification channel to their chosen push transport.",
373        ],
374        deprecation: None,
375    },
376    UiProtocolCapabilitySpec {
377        protocol: "mcp_apps",
378        versions: &["2026-01-26", "2025-11-21"],
379        implementation_tier: UiProtocolImplementationTier::NativeSubset,
380        spec_track: UiProtocolSpecTrack::Stable,
381        reviewed_on: UI_CAPABILITY_SCHEMA_VERSION,
382        version_details: &[
383            UiProtocolVersionSpec {
384                version: "2026-01-26",
385                spec_track: UiProtocolSpecTrack::Stable,
386                status: UiCapabilityStatus::Supported,
387                specification_url: "https://apps.extensions.modelcontextprotocol.io/",
388            },
389            UiProtocolVersionSpec {
390                version: "2025-11-21",
391                spec_track: UiProtocolSpecTrack::Candidate,
392                status: UiCapabilityStatus::Legacy,
393                specification_url: "https://github.com/modelcontextprotocol/ext-apps",
394            },
395        ],
396        negotiation: UiProtocolNegotiationSpec {
397            mode: "mcp_extension_negotiation",
398            capability_keys: &["capabilities.extensions.io.modelcontextprotocol/ui.mimeTypes"],
399            content_types: &["text/html;profile=mcp-app"],
400            transports: &["mcp_jsonrpc", "postmessage", "sandbox_proxy"],
401            fallback_protocols: &["text", "structured_content"],
402        },
403        capability_claims: &[
404            UiProtocolCapabilityClaim {
405                id: "progressive_enhancement",
406                status: UiCapabilityStatus::Supported,
407                scope: UiCapabilityScope::Runtime,
408                note: Some(
409                    "UI-enabled tools retain meaningful text and structured-content fallbacks.",
410                ),
411            },
412            UiProtocolCapabilityClaim {
413                id: "extension_mime_negotiation",
414                status: UiCapabilityStatus::Partial,
415                scope: UiCapabilityScope::Transport,
416                note: Some(
417                    "Capability shapes are emitted; host registration policy remains integration-owned.",
418                ),
419            },
420            UiProtocolCapabilityClaim {
421                id: "app_lifecycle",
422                status: UiCapabilityStatus::Partial,
423                scope: UiCapabilityScope::Host,
424                note: Some(
425                    "Initialize, initialized, message, and model-context flows are implemented; teardown/display-mode flows are typed but host-owned.",
426                ),
427            },
428            UiProtocolCapabilityClaim {
429                id: "sandbox_and_csp",
430                status: UiCapabilityStatus::HostOwned,
431                scope: UiCapabilityScope::Security,
432                note: Some(
433                    "Resource requirements are emitted and validated; the embedding host must enforce iframe isolation and CSP.",
434                ),
435            },
436            UiProtocolCapabilityClaim {
437                id: "tool_lifecycle_notifications",
438                status: UiCapabilityStatus::Partial,
439                scope: UiCapabilityScope::Client,
440                note: Some(
441                    "Tool input/result shapes are supported; complete postMessage orchestration remains host-owned.",
442                ),
443            },
444            UiProtocolCapabilityClaim {
445                id: "host_mediated_downloads_and_links",
446                status: UiCapabilityStatus::HostOwned,
447                scope: UiCapabilityScope::Host,
448                note: None,
449            },
450        ],
451        summary: "MCP Apps stable 2026-01-26 resource, negotiation, sandbox metadata, progressive fallback, and interactive app lifecycle subset.",
452        features: &[
453            "extension_id_io_modelcontextprotocol_ui",
454            "ui_resource_uri",
455            "tool_meta_ui_resource_uri",
456            "html_profile_mcp_app",
457            "structured_content",
458            "initialize_request",
459            "initialize_bridge_endpoint",
460            "message_bridge_endpoint",
461            "update_model_context_bridge_endpoint",
462            "bridge_host_context",
463            "bridge_app_capabilities",
464            "extension_capability_advertisement",
465            "progressive_enhancement_fallback",
466            "tool_lifecycle_notifications",
467            "host_context_changed",
468            "resource_teardown",
469            "resource_list_changed_capability",
470            "tool_list_changed_capability",
471            "inline_html_resource",
472            "sandbox_csp_meta",
473        ],
474        limitations: &[
475            "Full browser postMessage host SDK is host-owned; adk-ui provides payloads + HTTP bridge shapes used with adk-server.",
476            "Bridge session durability depends on the host (adk-server in-memory by default).",
477        ],
478        deprecation: None,
479    },
480];
481
482/// AWP protocol capability specification.
483#[cfg(feature = "awp")]
484pub const AWP_PROTOCOL_CAPABILITY: UiProtocolCapabilitySpec = UiProtocolCapabilitySpec {
485    protocol: "awp",
486    versions: &["1.0"],
487    implementation_tier: UiProtocolImplementationTier::CompatibilitySubset,
488    spec_track: UiProtocolSpecTrack::Draft,
489    reviewed_on: UI_CAPABILITY_SCHEMA_VERSION,
490    version_details: &[UiProtocolVersionSpec {
491        version: "1.0",
492        spec_track: UiProtocolSpecTrack::Draft,
493        status: UiCapabilityStatus::Partial,
494        specification_url: "https://github.com/zavora-ai/adk-rust",
495    }],
496    negotiation: UiProtocolNegotiationSpec {
497        mode: "capability_manifest",
498        capability_keys: &["capabilities"],
499        content_types: &["text/html", "application/json"],
500        transports: &["http"],
501        fallback_protocols: &["adk_ui"],
502    },
503    capability_claims: &[
504        UiProtocolCapabilityClaim {
505            id: "capability_manifest",
506            status: UiCapabilityStatus::Supported,
507            scope: UiCapabilityScope::Runtime,
508            note: None,
509        },
510        UiProtocolCapabilityClaim {
511            id: "interactive_hydration",
512            status: UiCapabilityStatus::HostOwned,
513            scope: UiCapabilityScope::Client,
514            note: None,
515        },
516    ],
517    summary: "AWP-aligned HTML rendering with capability manifest export and bandwidth-adaptive output.",
518    features: &[
519        "html_rendering",
520        "capability_manifest_export",
521        "bandwidth_adaptive",
522        "tool_envelope_bridge",
523    ],
524    limitations: &[
525        "HTML renderer produces static markup; interactive behaviors require client-side hydration.",
526        "Chart components render as data-attribute placeholders, not visual charts.",
527        "Site-level AWP (discovery, trust, rate limits) lives in adk-awp; this crate supplies UI payload/HTML only.",
528    ],
529    deprecation: None,
530};
531
532/// Normalize runtime UI profile aliases to canonical values.
533pub fn normalize_runtime_ui_protocol(raw: &str) -> Option<&'static str> {
534    match raw.trim().to_ascii_lowercase().as_str() {
535        "adk_ui" => Some("adk_ui"),
536        "a2ui" => Some("a2ui"),
537        "ag_ui" | "ag-ui" => Some("ag_ui"),
538        "mcp_apps" | "mcp-apps" => Some("mcp_apps"),
539        #[cfg(feature = "awp")]
540        "awp" => Some("awp"),
541        _ => None,
542    }
543}
544
545#[cfg(test)]
546mod tests {
547    use super::*;
548
549    #[test]
550    fn normalize_runtime_protocol_accepts_aliases() {
551        assert_eq!(normalize_runtime_ui_protocol("adk_ui"), Some("adk_ui"));
552        assert_eq!(normalize_runtime_ui_protocol("A2UI"), Some("a2ui"));
553        assert_eq!(normalize_runtime_ui_protocol("ag-ui"), Some("ag_ui"));
554        assert_eq!(normalize_runtime_ui_protocol("mcp-apps"), Some("mcp_apps"));
555        assert_eq!(normalize_runtime_ui_protocol("unknown"), None);
556    }
557
558    #[test]
559    fn capability_specs_cover_supported_protocols() {
560        let protocols: Vec<&str> = UI_PROTOCOL_CAPABILITIES
561            .iter()
562            .map(|spec| spec.protocol)
563            .collect();
564        assert_eq!(protocols, SUPPORTED_UI_PROTOCOLS);
565    }
566
567    #[test]
568    fn capability_specs_include_versions() {
569        for spec in UI_PROTOCOL_CAPABILITIES {
570            assert!(
571                !spec.versions.is_empty(),
572                "missing versions for {}",
573                spec.protocol
574            );
575            assert!(
576                !spec.features.is_empty(),
577                "missing features for {}",
578                spec.protocol
579            );
580            assert!(
581                !spec.summary.trim().is_empty(),
582                "missing summary for {}",
583                spec.protocol
584            );
585            assert!(
586                !spec.limitations.is_empty(),
587                "missing limitations for {}",
588                spec.protocol
589            );
590            assert_eq!(spec.reviewed_on, UI_CAPABILITY_SCHEMA_VERSION);
591            assert!(
592                !spec.version_details.is_empty(),
593                "missing version details for {}",
594                spec.protocol
595            );
596            assert!(
597                !spec.capability_claims.is_empty(),
598                "missing scoped claims for {}",
599                spec.protocol
600            );
601            assert!(
602                !spec.negotiation.transports.is_empty(),
603                "missing transports for {}",
604                spec.protocol
605            );
606        }
607    }
608
609    #[test]
610    fn legacy_profile_has_deprecation_metadata() {
611        let legacy = UI_PROTOCOL_CAPABILITIES
612            .iter()
613            .find(|spec| spec.protocol == "adk_ui")
614            .expect("adk_ui capability");
615        let deprecation = legacy.deprecation.expect("adk_ui deprecation metadata");
616        assert_eq!(deprecation.announced_on, "2026-02-07");
617        assert_eq!(deprecation.sunset_target_on, Some("2026-12-31"));
618    }
619
620    #[test]
621    fn modern_protocols_are_native_subset() {
622        for name in ["a2ui", "ag_ui", "mcp_apps"] {
623            let spec = UI_PROTOCOL_CAPABILITIES
624                .iter()
625                .find(|s| s.protocol == name)
626                .unwrap_or_else(|| panic!("missing {name}"));
627            assert_eq!(
628                spec.implementation_tier,
629                UiProtocolImplementationTier::NativeSubset,
630                "{name}"
631            );
632            assert_eq!(spec.spec_track, UiProtocolSpecTrack::Stable, "{name}");
633        }
634        let a2ui = UI_PROTOCOL_CAPABILITIES
635            .iter()
636            .find(|s| s.protocol == "a2ui")
637            .unwrap();
638        assert!(a2ui.versions.contains(&"0.9.1"));
639        assert!(a2ui.version_details.iter().any(|version| {
640            version.version == "1.0"
641                && version.spec_track == UiProtocolSpecTrack::Candidate
642                && version.status == UiCapabilityStatus::Candidate
643        }));
644
645        let mcp_apps = UI_PROTOCOL_CAPABILITIES
646            .iter()
647            .find(|s| s.protocol == "mcp_apps")
648            .unwrap();
649        assert_eq!(mcp_apps.versions.first(), Some(&"2026-01-26"));
650
651        let ag_ui = UI_PROTOCOL_CAPABILITIES
652            .iter()
653            .find(|s| s.protocol == "ag_ui")
654            .unwrap();
655        assert!(ag_ui.capability_claims.iter().any(|claim| {
656            claim.id == "interrupt_resume" && claim.status == UiCapabilityStatus::Partial
657        }));
658    }
659
660    #[test]
661    fn preferred_protocol_is_a2ui() {
662        assert_eq!(UI_PREFERRED_PROTOCOL, "a2ui");
663        assert!(normalize_runtime_ui_protocol(UI_PREFERRED_PROTOCOL).is_some());
664    }
665}