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            "surface_activity_snapshot",
365            "run_error",
366            "raw_events",
367        ],
368        limitations: &[
369            "Example hosts may still wrap some ADK runtime events; prefer protocol_native SSE when available from adk-server.",
370            "Durable checkpointing and approval policy for interrupt/resume remain application-defined.",
371            "WebSocket, binary HTTP, push notification, and resumable sequence transports are not implemented by the example server.",
372        ],
373        deprecation: None,
374    },
375    UiProtocolCapabilitySpec {
376        protocol: "mcp_apps",
377        versions: &["2026-01-26", "2025-11-21"],
378        implementation_tier: UiProtocolImplementationTier::NativeSubset,
379        spec_track: UiProtocolSpecTrack::Stable,
380        reviewed_on: UI_CAPABILITY_SCHEMA_VERSION,
381        version_details: &[
382            UiProtocolVersionSpec {
383                version: "2026-01-26",
384                spec_track: UiProtocolSpecTrack::Stable,
385                status: UiCapabilityStatus::Supported,
386                specification_url: "https://apps.extensions.modelcontextprotocol.io/",
387            },
388            UiProtocolVersionSpec {
389                version: "2025-11-21",
390                spec_track: UiProtocolSpecTrack::Candidate,
391                status: UiCapabilityStatus::Legacy,
392                specification_url: "https://github.com/modelcontextprotocol/ext-apps",
393            },
394        ],
395        negotiation: UiProtocolNegotiationSpec {
396            mode: "mcp_extension_negotiation",
397            capability_keys: &["capabilities.extensions.io.modelcontextprotocol/ui.mimeTypes"],
398            content_types: &["text/html;profile=mcp-app"],
399            transports: &["mcp_jsonrpc", "postmessage", "sandbox_proxy"],
400            fallback_protocols: &["text", "structured_content"],
401        },
402        capability_claims: &[
403            UiProtocolCapabilityClaim {
404                id: "progressive_enhancement",
405                status: UiCapabilityStatus::Supported,
406                scope: UiCapabilityScope::Runtime,
407                note: Some(
408                    "UI-enabled tools retain meaningful text and structured-content fallbacks.",
409                ),
410            },
411            UiProtocolCapabilityClaim {
412                id: "extension_mime_negotiation",
413                status: UiCapabilityStatus::Partial,
414                scope: UiCapabilityScope::Transport,
415                note: Some(
416                    "Capability shapes are emitted; host registration policy remains integration-owned.",
417                ),
418            },
419            UiProtocolCapabilityClaim {
420                id: "app_lifecycle",
421                status: UiCapabilityStatus::Partial,
422                scope: UiCapabilityScope::Host,
423                note: Some(
424                    "Initialize, initialized, message, and model-context flows are implemented; teardown/display-mode flows are typed but host-owned.",
425                ),
426            },
427            UiProtocolCapabilityClaim {
428                id: "sandbox_and_csp",
429                status: UiCapabilityStatus::HostOwned,
430                scope: UiCapabilityScope::Security,
431                note: Some(
432                    "Resource requirements are emitted and validated; the embedding host must enforce iframe isolation and CSP.",
433                ),
434            },
435            UiProtocolCapabilityClaim {
436                id: "tool_lifecycle_notifications",
437                status: UiCapabilityStatus::Partial,
438                scope: UiCapabilityScope::Client,
439                note: Some(
440                    "Tool input/result shapes are supported; complete postMessage orchestration remains host-owned.",
441                ),
442            },
443            UiProtocolCapabilityClaim {
444                id: "host_mediated_downloads_and_links",
445                status: UiCapabilityStatus::HostOwned,
446                scope: UiCapabilityScope::Host,
447                note: None,
448            },
449        ],
450        summary: "MCP Apps stable 2026-01-26 resource, negotiation, sandbox metadata, progressive fallback, and interactive app lifecycle subset.",
451        features: &[
452            "extension_id_io_modelcontextprotocol_ui",
453            "ui_resource_uri",
454            "tool_meta_ui_resource_uri",
455            "html_profile_mcp_app",
456            "structured_content",
457            "initialize_request",
458            "initialize_bridge_endpoint",
459            "message_bridge_endpoint",
460            "update_model_context_bridge_endpoint",
461            "bridge_host_context",
462            "bridge_app_capabilities",
463            "extension_capability_advertisement",
464            "progressive_enhancement_fallback",
465            "tool_lifecycle_notifications",
466            "host_context_changed",
467            "resource_teardown",
468            "resource_list_changed_capability",
469            "tool_list_changed_capability",
470            "inline_html_resource",
471            "sandbox_csp_meta",
472        ],
473        limitations: &[
474            "Full browser postMessage host SDK is host-owned; adk-ui provides payloads + HTTP bridge shapes used with adk-server.",
475            "Bridge session durability depends on the host (adk-server in-memory by default).",
476        ],
477        deprecation: None,
478    },
479];
480
481/// AWP protocol capability specification.
482#[cfg(feature = "awp")]
483pub const AWP_PROTOCOL_CAPABILITY: UiProtocolCapabilitySpec = UiProtocolCapabilitySpec {
484    protocol: "awp",
485    versions: &["1.0"],
486    implementation_tier: UiProtocolImplementationTier::CompatibilitySubset,
487    spec_track: UiProtocolSpecTrack::Draft,
488    reviewed_on: UI_CAPABILITY_SCHEMA_VERSION,
489    version_details: &[UiProtocolVersionSpec {
490        version: "1.0",
491        spec_track: UiProtocolSpecTrack::Draft,
492        status: UiCapabilityStatus::Partial,
493        specification_url: "https://github.com/zavora-ai/adk-rust",
494    }],
495    negotiation: UiProtocolNegotiationSpec {
496        mode: "capability_manifest",
497        capability_keys: &["capabilities"],
498        content_types: &["text/html", "application/json"],
499        transports: &["http"],
500        fallback_protocols: &["adk_ui"],
501    },
502    capability_claims: &[
503        UiProtocolCapabilityClaim {
504            id: "capability_manifest",
505            status: UiCapabilityStatus::Supported,
506            scope: UiCapabilityScope::Runtime,
507            note: None,
508        },
509        UiProtocolCapabilityClaim {
510            id: "interactive_hydration",
511            status: UiCapabilityStatus::HostOwned,
512            scope: UiCapabilityScope::Client,
513            note: None,
514        },
515    ],
516    summary: "AWP-aligned HTML rendering with capability manifest export and bandwidth-adaptive output.",
517    features: &[
518        "html_rendering",
519        "capability_manifest_export",
520        "bandwidth_adaptive",
521        "tool_envelope_bridge",
522    ],
523    limitations: &[
524        "HTML renderer produces static markup; interactive behaviors require client-side hydration.",
525        "Chart components render as data-attribute placeholders, not visual charts.",
526        "Site-level AWP (discovery, trust, rate limits) lives in adk-awp; this crate supplies UI payload/HTML only.",
527    ],
528    deprecation: None,
529};
530
531/// Normalize runtime UI profile aliases to canonical values.
532pub fn normalize_runtime_ui_protocol(raw: &str) -> Option<&'static str> {
533    match raw.trim().to_ascii_lowercase().as_str() {
534        "adk_ui" => Some("adk_ui"),
535        "a2ui" => Some("a2ui"),
536        "ag_ui" | "ag-ui" => Some("ag_ui"),
537        "mcp_apps" | "mcp-apps" => Some("mcp_apps"),
538        #[cfg(feature = "awp")]
539        "awp" => Some("awp"),
540        _ => None,
541    }
542}
543
544#[cfg(test)]
545mod tests {
546    use super::*;
547
548    #[test]
549    fn normalize_runtime_protocol_accepts_aliases() {
550        assert_eq!(normalize_runtime_ui_protocol("adk_ui"), Some("adk_ui"));
551        assert_eq!(normalize_runtime_ui_protocol("A2UI"), Some("a2ui"));
552        assert_eq!(normalize_runtime_ui_protocol("ag-ui"), Some("ag_ui"));
553        assert_eq!(normalize_runtime_ui_protocol("mcp-apps"), Some("mcp_apps"));
554        assert_eq!(normalize_runtime_ui_protocol("unknown"), None);
555    }
556
557    #[test]
558    fn capability_specs_cover_supported_protocols() {
559        let protocols: Vec<&str> = UI_PROTOCOL_CAPABILITIES
560            .iter()
561            .map(|spec| spec.protocol)
562            .collect();
563        assert_eq!(protocols, SUPPORTED_UI_PROTOCOLS);
564    }
565
566    #[test]
567    fn capability_specs_include_versions() {
568        for spec in UI_PROTOCOL_CAPABILITIES {
569            assert!(
570                !spec.versions.is_empty(),
571                "missing versions for {}",
572                spec.protocol
573            );
574            assert!(
575                !spec.features.is_empty(),
576                "missing features for {}",
577                spec.protocol
578            );
579            assert!(
580                !spec.summary.trim().is_empty(),
581                "missing summary for {}",
582                spec.protocol
583            );
584            assert!(
585                !spec.limitations.is_empty(),
586                "missing limitations for {}",
587                spec.protocol
588            );
589            assert_eq!(spec.reviewed_on, UI_CAPABILITY_SCHEMA_VERSION);
590            assert!(
591                !spec.version_details.is_empty(),
592                "missing version details for {}",
593                spec.protocol
594            );
595            assert!(
596                !spec.capability_claims.is_empty(),
597                "missing scoped claims for {}",
598                spec.protocol
599            );
600            assert!(
601                !spec.negotiation.transports.is_empty(),
602                "missing transports for {}",
603                spec.protocol
604            );
605        }
606    }
607
608    #[test]
609    fn legacy_profile_has_deprecation_metadata() {
610        let legacy = UI_PROTOCOL_CAPABILITIES
611            .iter()
612            .find(|spec| spec.protocol == "adk_ui")
613            .expect("adk_ui capability");
614        let deprecation = legacy.deprecation.expect("adk_ui deprecation metadata");
615        assert_eq!(deprecation.announced_on, "2026-02-07");
616        assert_eq!(deprecation.sunset_target_on, Some("2026-12-31"));
617    }
618
619    #[test]
620    fn modern_protocols_are_native_subset() {
621        for name in ["a2ui", "ag_ui", "mcp_apps"] {
622            let spec = UI_PROTOCOL_CAPABILITIES
623                .iter()
624                .find(|s| s.protocol == name)
625                .unwrap_or_else(|| panic!("missing {name}"));
626            assert_eq!(
627                spec.implementation_tier,
628                UiProtocolImplementationTier::NativeSubset,
629                "{name}"
630            );
631            assert_eq!(spec.spec_track, UiProtocolSpecTrack::Stable, "{name}");
632        }
633        let a2ui = UI_PROTOCOL_CAPABILITIES
634            .iter()
635            .find(|s| s.protocol == "a2ui")
636            .unwrap();
637        assert!(a2ui.versions.contains(&"0.9.1"));
638        assert!(a2ui.version_details.iter().any(|version| {
639            version.version == "1.0"
640                && version.spec_track == UiProtocolSpecTrack::Candidate
641                && version.status == UiCapabilityStatus::Candidate
642        }));
643
644        let mcp_apps = UI_PROTOCOL_CAPABILITIES
645            .iter()
646            .find(|s| s.protocol == "mcp_apps")
647            .unwrap();
648        assert_eq!(mcp_apps.versions.first(), Some(&"2026-01-26"));
649
650        let ag_ui = UI_PROTOCOL_CAPABILITIES
651            .iter()
652            .find(|s| s.protocol == "ag_ui")
653            .unwrap();
654        assert!(ag_ui.capability_claims.iter().any(|claim| {
655            claim.id == "interrupt_resume" && claim.status == UiCapabilityStatus::Partial
656        }));
657    }
658
659    #[test]
660    fn preferred_protocol_is_a2ui() {
661        assert_eq!(UI_PREFERRED_PROTOCOL, "a2ui");
662        assert!(normalize_runtime_ui_protocol(UI_PREFERRED_PROTOCOL).is_some());
663    }
664}