Technical spec · Alpha Release

How AnySDUI is built, and how to use it

Installation, tech stack, Google A2UI v1.0 compatibility and migration, architecture, and the full path from an annotated composable to a screen driven by a server or an agent.

Version 1.0.0-alpha01Google A2UI v1.0 Compatible (400/409)Apache License 2.0Group dev.fediim
Integration · 1.0.0-alpha01

Add it to an existing project in three steps

Works with any Kotlin Multiplatform project that uses Compose Multiplatform and KSP, or pure Android Jetpack Compose projects.

// components-sdui/build.gradle.kts in fediim-design-system
val anysduiVersion = "1.0.0-alpha01"

commonMain.dependencies {
    implementation("dev.fediim:anysdui-annotations:$anysduiVersion")
    api("dev.fediim:anysdui-runtime:$anysduiVersion")
}

ksp {
    arg("anysdui.catalogId", "urn:fediim:fds")
    arg("anysdui.libraryVersion", "0.1.0")
}

dependencies {
    add("kspCommonMainMetadata", "dev.fediim:anysdui-processor:$anysduiVersion")
}

Alpha Release (1.0.0-alpha01). Published to GitHub Packages under group dev.fediim at 1.0.0-alpha01 (and 1.0.0-SNAPSHOT). Pure Android projects without KMP can wrap binary AAR design systems directly.

Alpha release (1.0.0-alpha01)

Following Google's A2UI for Jetpack Compose showcase at Android Dev Summit (28 October 2026), AnySDUI 1.0.0-alpha01 is now available as our first tagged Alpha release under group dev.fediim. It ships a complete, compile-time checked A2UI v1.0 stack for Kotlin Multiplatform and pure Android Jetpack Compose.

  • 18 published artifacts under dev.fediim:anysdui-*:1.0.0-alpha01 covering annotations, KSP2 processor, wire protocol, strict validator, Compose runtime, Ktor WebSocket & A2A 0.3/1.0 transports, Kotlin agent SDK, Compose UI test kit, and 8 ready-made catalogs (plus custom KSP catalogs).
  • Google A2UI v1.0 wire compatibility: checked against upstream commit ccf5dc20d0 with 400 of 409 applicable conformance cases passing in CI.
  • 6 native targets from one Kotlin codebase: Android (minSdk 28), Pure Android (binary AAR wrapper modules), iOS (iosArm64, iosSimulatorArm64), Desktop JVM, Web WasmJs, and Web JS.
  • Design-to-device workflows (RFC-0012): Direct Figma frame push routes, server-side composite expansion, Follow/Explore multi-screen flow navigation (anysdui.navigate), and HMAC-SHA256 signed zero-install web viewer links.

Google A2UI compatibility & migration

Google's A2UI protocol and its AndroidX Jetpack Compose libraries (androidx.a2ui:*, androidx.a2ui.compose:* and androidx.compose.material3:material3-a2ui) define a declarative JSON standard for agent-generated UIs. Google's renderer targets A2UI v0.9 (its basic catalog id points to v0_9). AnySDUI implements A2UI v1.0 and does not read v0.9 messages, so an agent written for Google's renderer needs to move to v1.0 first. On top of the protocol, AnySDUI removes the two biggest bottlenecks teams face when adopting A2UI in production: manual catalog/renderer glue and single-platform lock-in.

Artifact mapping: Google androidx.a2ui vs AnySDUI

RoleGoogle AndroidX (1.0.0-alpha02)AnySDUI (1.0.0-alpha01)What AnySDUI adds
Wire modelandroidx.a2ui:a2ui-modeldev.fediim:anysdui-protocolMultiplatform (Android, iOS, JVM, WasmJs, JS); zero UI/network deps
Engine & validationandroidx.a2ui:a2ui-enginedev.fediim:anysdui-validationSTRICT & LENIENT modes, @path/@call/checks, ThemeSpec & function bounds
Compose runtimeandroidx.a2ui.compose:compose-runtimedev.fediim:anysdui-runtimeCompose Multiplatform + Pure Android, RenderScope.WithTheme subtree theming and DTCG motion
Custom catalog APIandroidx.a2ui.compose:compose-ui (manual A2uiComponent / A2uiProperty)dev.fediim:anysdui-annotations + anysdui-processor (KSP2)Compile-time generation of both CatalogRenderer and catalog.json from @SduiComponent; build-time @SduiExample validation
Basic catalog (18 components)androidx.compose.material3:material3-a2uidev.fediim:anysdui-compose-basicThe upstream A2UI v1.0 basic catalog, with schemas equal to upstream's, on all 6 targets
Extended catalogsNot includedanysdui-compose-material3 (60), material (20), foundation (17), cupertino (28), charts (4), 3d (1), xr (1)149 components in total out of the box; mix multiple catalogs on one SurfaceHost
Agent SDK & A2AExternal / manualanysdui-agent, anysdui-a2a, anysdui-agent-a2aStreaming parser, structured validation repair hints, A2A 0.3 & 1.0 client/server, generated Claude/Gemini/ADK skills
Component test kitandroidx.a2ui.compose:compose-ui-testing (A2uiTestSurface)dev.fediim:anysdui-compose-testingSduiTestController with node stubbing, data model inspection & action assertions

Conformance and the 9 known differences

AnySDUI runs Google's upstream A2UI v1.0 RC conformance suite (pinned at ccf5dc20d0) in CI with make test-a2ui-conformance. Of 409 applicable cases, 400 pass. The build fails if any other case fails. The 9 known differences are listed, each with its issue:

  • Agent function call ids (3 cases): the host generates the id of every callAgentFunction and accepts none from the caller, and a handler that throws is answered with INVALID_FUNCTION_CALL.
  • Bindings need @path (2 cases): a bare {"path": …} object stays plain data. Only {"@path": …} binds, as upstream's own reserved-keys rules say.
  • Basic catalog at the pin (2 cases): the pinned basic catalog has no Button.title, and CheckBox keeps its state in value.
  • User activation (1 case): a call sent by an agent never counts as a user gesture.
  • Unknown catalog error (1 case): an unresolvable catalog id is reported as a top-level error, not inside the function response.

Migrating a custom component from androidx.a2ui.compose

If you started prototyping with Google's androidx.a2ui.compose, replace manual A2uiComponent and A2uiProperty blocks with @SduiComponent directly on your Composable:

Before (androidx.a2ui.compose) → After (AnySDUI KSP)
// BEFORE: Google androidx.a2ui.compose (manual schema + manual extraction)
val statusCard = A2uiComponent(
    name = "StatusCard",
    properties = listOf(
        A2uiProperty.string("headline", required = true),
        A2uiProperty.boolean("urgent", required = false),
        A2uiProperty.action("onDismiss", required = false),
    )
) { scope, node ->
    StatusCard(
        headline = scope.resolveString(node, "headline") ?: "",
        urgent = scope.resolveBoolean(node, "urgent") ?: false,
        onDismiss = { scope.dispatchAction(node, "onDismiss") },
    )
}

// AFTER: AnySDUI (compile-time KSP generates renderer, catalog.json & agent skill)
@SduiUsage("Displays an operational status alert with optional dismiss action.")
@SduiComponent
@Composable
fun StatusCard(
    headline: String,
    @SduiDefault("false") urgent: Boolean = false,
    @SduiUserGesture onDismiss: () -> Unit = {},
) { ... }
Google A2UI for Compose vs AnySDUI

Built on Google's A2UI protocol. Designed to go much further.

Google open-sourced the A2UI protocol in December 2025. At Android Dev Summit on 28 October 2026, it showcased A2UI for Jetpack Compose (androidx.a2ui), in alpha since September 2026. That renderer targets A2UI v0.9. AnySDUI implements the newer A2UI v1.0 and passes 400 of 409 applicable upstream conformance cases. It also removes manual component registration and brings your design system to 6 platforms.

400/409Google upstream A2UI v1.0 RC conformance cases passing in CI
0 linesof hand-written JSON schema or property-mapping glue with KSP
149 vs 18ready-to-render components across 8 catalogs vs 1 Basic catalog
Google androidx.a2ui.compose · Manual schema & glue
// In Google's library, every custom Composable needs manual
// A2uiProperty descriptors, JSON extraction and catalog wiring:
val metricCardComponent = A2uiComponent(
    name = "MetricCard",
    properties = listOf(
        A2uiProperty.string("title", required = true),
        A2uiProperty.string("value", required = true),
        A2uiProperty.string("trend", required = false),
        A2uiProperty.action("onInspect", required = false),
    )
) { scope, node ->
    val title = scope.resolveString(node, "title") ?: return@A2uiComponent
    val value = scope.resolveString(node, "value") ?: return@A2uiComponent
    val trend = scope.resolveString(node, "trend") ?: "neutral"
    MetricCard(
        title = title,
        value = value,
        trend = Trend.valueOf(trend.uppercase()),
        onInspect = { scope.dispatchAction(node, "onInspect") }
    )
}

// Then register manually (Android-only):
val catalog = A2uiCatalog(
    catalogId = "https://acme.com/catalog/v1",
    components = MaterialA2uiBasicCatalogV1Defaults.all + metricCardComponent
)
AnySDUI · Compile-time KSP from your Composable
// In AnySDUI, annotate the Composable you already have.
// KSP generates both CatalogRenderer AND catalog.json at compile time:
@SduiUsage("Displays a KPI metric with trend badge and drill-down action.")
@SduiComponent
@Composable
fun MetricCard(
    title: String,
    value: String,
    @SduiDefault("Trend.NEUTRAL") trend: Trend = Trend.NEUTRAL,
    @SduiUserGesture onInspect: () -> Unit = {},
    modifier: Modifier = Modifier,
) {
    // Your existing Compose Multiplatform or Jetpack Compose UI code
}

// Zero manual property extraction.
// Zero schema drift: changing a Kotlin parameter updates catalog.json,
// strict validator rules, and generated Claude/Gemini/ADK skills in one build.
// Works on Android, Pure Android AARs, iOS, Desktop JVM, Web Wasm & JS.
Feature-by-feature comparison: Google A2UI for Compose vs AnySDUI 1.0.0-alpha01
Capability Google A2UI (androidx.a2ui 1.0.0-alpha02) AnySDUI (dev.fediim.anysdui 1.0.0-alpha01)
A2UI wire protocol v0.9 Basic catalog id points to v0_9 Yes · 400/409 Pinned to upstream v1.0 RC (ccf5dc20d0) with @path, @call & checks verified in CI; v0.9 is not supported
Basic Catalog (18 components) Yes androidx.compose.material3:material3-a2ui Yes dev.fediim:anysdui-compose-basic (for agents that write A2UI v1.0)
Custom design system onboarding Manual glue Hand-written A2uiComponent, A2uiProperty schemas & state extractors Compile-time KSP Add @SduiComponent to existing @Composable functions; processor generates renderer & catalog.json
Compile-time example & drift checks No No build-time validation of example payloads Yes Renderer & schema come from the same signature; @SduiExample payloads are validated during build
Platforms from one Compose codebase Android only Requires separate Flutter (Dart), Web (Lit/React/Angular) or SwiftUI codebases 6 targets Android, Pure Android (binary AAR wrappers), iOS, Desktop JVM, Web WasmJs & Web JS
Built-in production catalogs 1 catalog Basic Catalog (18 components) 8 catalogs · 149 components Basic (18), Material 3 (60), Material 2 (20), Foundation (17), Cupertino (28), Charts (4), 3D (1), Android XR (1) + custom
Multi-catalog composition Merged map Single merged catalog per surface Per-node namespace SurfaceHost routes each node by its own catalogId with zero name collisions
Runtime theming & W3C tokens Basic Surface properties & host MaterialTheme Full + Subtree Runtime setTheme/setThemeMode, WithTheme subtree overrides, ThemePolicy & W3C DTCG 2025.10 importer
Motion & reduced-motion policy Manual Hand-coded per component Declarative DTCG Tokenized durations, easings, springs, automatic state & list enter/exit animations + accessibility MotionPolicy
Multi-screen flows & Figma push No Single-surface agent rendering RFC-0012 built-in Direct Figma frame push, composite expansion, Follow/Explore anysdui.navigate flows & HMAC-signed web share links
Agent SDK, skills & UI test kit Client engine a2ui-engine runtime validation and compose-ui-testing (A2uiTestSurface) End-to-end Multiplatform anysdui-agent SDK with repair hints, auto-generated Claude/Gemini/ADK skills, A2A 1.0 & anysdui-compose-testing

Tech stack

AnySDUI is a set of Kotlin Multiplatform libraries. The client side is Compose Multiplatform. The protocol, validation and agent libraries are plain Kotlin with no UI or network framework, so a server or an agent can use them alone.

LayerTechnologyVersion on main
LanguageKotlin Multiplatform2.4.0
UICompose Multiplatform (plus pure Android Jetpack Compose wrappers)1.12.0
Code generationKSP2 processor, KotlinPoetKSP 2.3.10
Serializationkotlinx-serialization-json1.11.0
Concurrencykotlinx-coroutines1.11.0
NetworkingKtor client, only in the transport, A2A and image or model loading modules3.5.2
3DSceneView, behind the opt-in 3D catalog4.35.0
ChartsVico, behind the opt-in charts catalog3.3.1
Component testingCompose ui-test, in anysdui-compose-testing1.12.0
AndroidAndroid Gradle Plugin, minSdk 28 (API 28 verification floor)AGP 9.4.0
JDK21, or 22 for a Desktop build that includes the 3D catalog21
Sample serverKtor server, Koog for AI writers, Exposed + Flyway for databaseKtor 3.5.2 · Koog 1.2.0

Targets

Android · minSdk 28Pure Android (Jetpack Compose AAR wrappers)iOS · iosArm64, iosSimulatorArm64Desktop · JVMWeb · WasmJsWeb · JS

Standards

  • A2UI v1.0, pinned to upstream release-candidate commit ccf5dc20d0 with @path and @call syntax, checked against upstream's core conformance suite in CI (400 of 409 applicable cases pass; the other 9 are documented differences).
  • A2A 0.3 and 1.0 for talking to agents, and the server can itself be an A2A 1.0 agent.
  • JSON Pointer (RFC 6901) for data bindings, and JSON Schema for catalogs.
  • W3C Design Tokens (DTCG 2025.10) for importing themes (colors, typography, dimensions, and motion).
  • RFC-0012: Figma screens to devices push pipeline, composite expansion, Follow/Explore flow navigation, and signed share links.

Architecture

Each module depends only on the ones above it. Static guards in CI enforce the boundaries, for example that the protocol module never imports a UI or networking framework.

anysdui-annotationsThe annotations you put on your components. Zero dependencies.
anysdui-processorKSP processor, JVM only. Reads the annotations, writes catalog.json and the Kotlin renderer.
anysdui-protocolA2UI v1.0 message envelopes and serializers.
anysdui-validationCatalog model, message and catalog validators, expression and checks evaluators, theme model, function registry. No Compose, no Ktor.
anysdui-runtimeThe client engine on Compose: SurfaceState, SurfaceHost, SduiSurface, data model, motion animation, subtree theming (WithTheme), and the transport interface.
anysdui-transport-ktorBeside the runtime: WebSocketTransport on the Ktor client.
anysdui-a2aBeside the runtime: A2A wire models and a client that adapts an agent session to a transport.
anysdui-agentBeside the runtime: the agent SDK. Depends on protocol and validation only, never on the runtime.
anysdui-compose-testingBeside the runtime: component test kit (controller, stubs, assertions) on Compose ui-test.

Life of a session

HandshakeThe client connects and lists the catalog ids it supports. The server answers with the one it will use.
CreateThe server sends createSurface: a flat list of component nodes with ids, plus an initial data model. Children are lists of ids.
Interact & NavigateTyping updates the local data model at once, with no network. A click sends an action with its context, or anysdui.navigate to change flow frames.
PatchThe server answers with updateDataModel at a JSON Pointer path. Only composables that read that path recompose.

Messages & routes

ProtocolDirectionMessages & Endpoints
WebSocket (A2UI)Server to clientcreateSurface, updateComponents, updateDataModel, deleteSurface, callRendererFunction, agentFunctionResponse
WebSocket (A2UI)Client to serveraction (including anysdui.navigate for flows), callAgentFunction, rendererFunctionResponse, error
Push API (RFC-0012)Plugin to serverPOST /playground/push (atomic flow push), PUT /playground/push/images/{sha256}, POST /playground/push/presenter
Share linksBrowser to serverGET /view/{viewId} (web viewer), POST /playground/links, POST /playground/links/regenerate
createSurface
{
  "version": "v1.0",
  "createSurface": {
    "surfaceId": "surf_counter",
    "catalogId": "urn:anysdui:sample-designsystem:1.0.0",
    "metadata": { "extensions": { "anysdui_theme": { "mode": "dark" } } },
    "components": [
      { "id": "root_card", "component": "DsCard", "content": ["counter_text", "increment_btn"] },
      { "id": "counter_text", "component": "DsText", "text": { "@path": "/state/count" } },
      { "id": "increment_btn", "component": "DsButton",
        "onClick": { "event": { "name": "increment" } }, "content": ["btn_label"] },
      { "id": "btn_label", "component": "DsText", "text": "Increment Count" }
    ],
    "dataModel": { "state": { "count": 0 } }
  }
}

Artifacts

Every library is published under the group dev.fediim with one shared version. Take only what your side of the wire needs.

ArtifactWhat it isWho needs it
anysdui-annotations@SduiComponent and the other annotationsDesign system authors
anysdui-processorKSP processor that generates renderers and catalog.jsonDesign system authors
anysdui-runtimeSduiSurface, SurfaceHost, data model, rendering, subtree themingApps (KMP and pure Android)
anysdui-transport-ktorWebSocketTransportApps that connect over WebSocket
anysdui-a2aA2A 0.3 and 1.0 client and wire modelsApps that render an A2A agent
anysdui-protocolMessage types and serializersServers, agents
anysdui-validationCatalog model and validators, without ComposeServers, agents
anysdui-agentKotlin agent SDKAgents written in Kotlin
anysdui-agent-a2aA2A packaging and transport adapter for the agent SDKAgents connecting over A2A
anysdui-compose-foundationCompose Foundation catalog (16 components)Apps, optional
anysdui-compose-materialMaterial 2 catalog (20 components)Apps, optional
anysdui-compose-material3Material 3 catalog (60 components: core, content, navigation, and overlays)Apps, optional
anysdui-compose-basicThe A2UI v1.0 basic catalog, all 18 componentsApps that render standard A2UI agents
anysdui-compose-chartsCharts catalog (LineChart, BarChart, PieChart, Sparkline via Vico 3.3.1)Apps, optional
anysdui-compose-cupertinoiOS-style catalog (Switch, SegmentedControl, Slider, Button, ActivityIndicator, AlertDialog)Apps, optional
anysdui-compose-3dModel3D catalogApps, optional
anysdui-compose-testingTest kit for catalog components on Compose ui-testCatalog authors, apps (test scope only)

The sample server, the playground, the theme importer (with motion token import) and the demo apps live in the repository and are not published.

Installation

Prerequisites

  • A Kotlin Multiplatform module with the Compose Multiplatform, Compose compiler and KSP plugins.
  • JDK 21 or newer. Xcode on macOS for the iOS targets.
  • For GitHub Packages, a GitHub token with the read:packages scope.

1. Add a repository

The libraries are not on Maven Central. Use GitHub Packages, or build them into your local Maven repository.

settings.gradle.kts
dependencyResolutionManagement {
    repositories {
        maven {
            name = "GitHubPackages"
            url = uri("https://maven.pkg.github.com/fediim-dev/anysdui-kmp")
            credentials(PasswordCredentials::class)
        }
        google()
        mavenCentral()
    }
}

// Credentials come from the environment. Never commit them.
//   ORG_GRADLE_PROJECT_GitHubPackagesUsername = your GitHub user name
//   ORG_GRADLE_PROJECT_GitHubPackagesPassword = a token with read:packages

2. Add the dependencies

build.gradle.kts (Compose Multiplatform)
val anysduiVersion = "1.0.0-alpha01"

kotlin {
    sourceSets {
        commonMain.dependencies {
            // A ready-made catalog. It brings anysdui-runtime with it.
            implementation("dev.fediim:anysdui-compose-foundation:$anysduiVersion")
            // WebSocketTransport lives in its own artifact.
            implementation("dev.fediim:anysdui-transport-ktor:$anysduiVersion")
            // Only when you annotate your own components:
            implementation("dev.fediim:anysdui-annotations:$anysduiVersion")
        }
    }
}

dependencies {
    add("kspCommonMainMetadata", "dev.fediim:anysdui-processor:$anysduiVersion")
}

Android-only projects (Pure Jetpack Compose)

Projects that are not Kotlin Multiplatform and wrap a binary design system (an AAR you cannot annotate) consume AnySDUI through a wrapper module. AGP 9 compiles Kotlin itself and runs KSP directly on the debug variant.

wrapper/build.gradle.kts (Pure Android)
plugins {
    alias(libs.plugins.androidLibrary)
    alias(libs.plugins.composeCompiler)
    alias(libs.plugins.ksp)
}

val anysduiVersion = "1.0.0-alpha01"

android {
    namespace = "com.acme.sdui"
    compileSdk = 35
    defaultConfig { minSdk = 28 }
}

ksp {
    arg("anysdui.catalogId", "urn:acme:sdui")
    arg("anysdui.libraryVersion", anysduiVersion)
}

dependencies {
    implementation(project(":components-sdui"))
    implementation("dev.fediim:anysdui-annotations:$anysduiVersion")
    api("dev.fediim:anysdui-runtime:$anysduiVersion")
    ksp("dev.fediim:anysdui-processor:$anysduiVersion")
}

Let an agent do it. The repository ships an integrating-anysdui skill. Copy that folder into your project's agent skills folder and a coding agent can do the wiring on this page for you.

Create your own catalog

A catalog is the list of components a server may use, with a schema for each. You do not write it by hand. You annotate composables in your design system module and the processor generates it.

1. Wire the design system module

components-sdui/build.gradle.kts
kotlin {
    sourceSets {
        commonMain {
            // KSP runs on common metadata; its output must be added by hand.
            kotlin.srcDir(layout.buildDirectory.dir("generated/ksp/metadata/commonMain/kotlin"))
            dependencies {
                implementation("dev.fediim:anysdui-annotations:$anysduiVersion")
                api("dev.fediim:anysdui-runtime:$anysduiVersion") // the generated renderer implements CatalogRenderer
            }
        }
    }
}

ksp {
    arg("anysdui.catalogId", "urn:fediim:fds")
    arg("anysdui.libraryVersion", "0.1.0")
}

dependencies {
    add("kspCommonMainMetadata", "dev.fediim:anysdui-processor:$anysduiVersion")
}

// Every target compilation must wait for the common KSP pass.
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompilationTask<*>>().configureEach {
    if (name != "kspCommonMainKotlinMetadata") dependsOn("kspCommonMainKotlinMetadata")
}

The id a client and server exchange is <catalogId>:<libraryVersion>, here urn:fediim:fds:0.1.0.

2. Annotate components

components-sdui/…/SduiActions.kt
import dev.fediim.anysdui.annotations.*

@SduiComponent
@Composable
fun Button(
    @SduiUserGesture onClick: () -> Unit,
    variant: ButtonVariant,
    modifier: Modifier = Modifier,
    @SduiDefault("ButtonSize.Medium") size: ButtonSize = ButtonSize.Medium,
    @SduiDefault("true") enabled: Boolean = true,
    @SduiDefault("null") @SduiChild icon: (@Composable () -> Unit)? = null,
    @SduiDefault("null") text: String? = null,
    @SduiDefault("{}") @SduiChildren content: @Composable RowScope.() -> Unit = {
        if (text != null) Text(text)
    },
) { ... }
AnnotationWhat it does
@SduiComponentExposes a public @Composable. The name defaults to the function name. KDoc becomes the description agents read.
@SduiDefault("expr")Required on every optional parameter except a Modifier. Injected into the generated renderer; resolves simple and nested (Inner.X) types.
@SduiChildren, @SduiChildA composable slot that takes a list of children, or exactly one child.
@SduiUserGestureMarks a callback fired only by the user's own input. Its action may then run functions that need a user activation, such as openUrl.
@SduiResThe parameter takes a theme token (colour, typography, icon) instead of a literal value.
@SduiThemeRegisters an object of tokens. Emits schemas into anysdui_types.<slug>.json and catalog.bundle.json.
@SduiReadyWhenResolvedThe component is not drawn until this value has arrived. The surface shows its loading slot instead.
@SduiCheckableThe component accepts validation checks and has an error state.
@SduiSchemaReplaces a parameter's generated schema with JSON you write, for shapes Kotlin types cannot express.
@SduiUsage, @SduiExampleTell an agent when to pick the component and show a valid message that uses it.
@SduiPreferredMarks preferred components for agents when multiple catalog components match a role.
@SduiCatalogCatalog-level metadata, as an alternative to the KSP options.

3. Declare theme tokens

components-sdui/…/DsSupport.kt (fediim)
// Tokens read the live theme, so they follow every fediim palette
@SduiTheme(ResType.COLOR)
object FdsSduiColors {
    val bgCard: Color
        @Composable get() = FediimTheme.colors.bgCard
    val textPrimary: Color
        @Composable get() = FediimTheme.colors.textPrimary
    // ...
}

@SduiTheme(ResType.TYPOGRAPHY)
object FdsSduiTypography {
    val headline: TextStyle
        @Composable get() = FediimTheme.typography.headline
    val body: TextStyle
        @Composable get() = FediimTheme.typography.body
    // ...
}

A component asks for a token with @SduiRes. The foundation catalog's BasicText does it like this:

anysdui-compose-foundation/…/TextAndInputs.kt
@SduiComponent
@Composable
fun BasicText(
    text: String,
    @SduiRes(ResType.TYPOGRAPHY)
    @SduiDefault("androidx.compose.ui.text.TextStyle.Default")
    style: TextStyle = TextStyle.Default,
    @SduiRes(ResType.COLOR)
    @SduiDefault("androidx.compose.ui.graphics.Color.Unspecified")
    color: Color = Color.Unspecified,
    modifier: Modifier = Modifier,
) { ... }

4. Two-way inputs need no annotation

A component with a value parameter and an onValueChange of the same type is bound to the data model in both directions. Supported types are String, Boolean, Float, Double and List<String>. Input is stored only when the message binds value to a path, such as {"@path": "/inputs/name"}. A literal value is shown read-only.

5. Teach agents your catalog

Examples and instructions travel inside the catalog, so a model can produce valid screens without extra prompting.

DsText.kt and build.gradle.kts
@SduiExample(
    name = "greeting",
    json = """[
      {"version": "v1.0", "createSurface": {"surfaceId": "greeting",
        "catalogId": "urn:anysdui:sample-designsystem:1.0.0"}},
      {"version": "v1.0", "updateComponents": {"surfaceId": "greeting", "components": [
        {"id": "root", "component": "DsText", "text": "Hello"}
      ]}}
    ]"""
)
@SduiComponent
@Composable
fun DsText(text: String) { ... }

// build.gradle.kts: layout rules and conventions for the whole catalog
ksp {
    arg("anysdui.instructions",
        providers.fileContents(layout.projectDirectory.file("catalog-instructions.md")).asText.get())
}

The processor checks every example's structure at build time. An invalid example fails the build and names the exact JSON Pointer.

6. What the build generates

OutputLocation under build/generated/ksp/metadata/commonMain/
Rendererkotlin/<package>/anysdui/generated/<Slug>Renderer.kt, an object that implements CatalogRenderer. For urn:fediim:fds it is FdsRenderer.
Catalogresources/catalog.json, an A2UI v1.0 catalog definition
Extension typesresources/anysdui_types.<slug>.json: modifiers and colour, typography, icon and shape references
Examplesresources/examples/<name>.json

Ship the catalog yourself. catalog.json is not packaged into any artifact. Copy or publish it to wherever your server or agent validates and prompts from, and refresh that copy whenever a component signature changes.

Keep the design system clean

If you do not want AnySDUI imports in your core library, put the annotated wrappers in a separate bridge module that depends on it. The fediim design system does this with its components-sdui module.

Render in an app

One catalog

Screen.kt
val catalogId = "urn:fediim:fds:0.1.0"

val transport = remember {
    WebSocketTransport(host = "localhost", port = 8081, supportedCatalogIds = listOf(catalogId)) // path defaults to /a2ui
}
val state = remember(transport) {
    SurfaceState(surfaceId = "screen", catalogId = catalogId, transport = transport)
}
LaunchedEffect(state, transport) { transport.incoming.collect { state.apply(it) } }

SduiSurface(state = state, renderer = FdsRenderer)

Several catalogs on one surface

Register each renderer together with its validator. A component resolves its catalog by its own catalogId, otherwise the surface default. There is no fallback by name, so two catalogs may both declare Row.

Screen.kt
val host = remember(transport) {
    SurfaceHost(
        transport = transport,
        registrations = listOf(
            CatalogRegistration(FdsRenderer, CatalogValidator.fromJsonString(fdsCatalogJson)),
            CatalogRegistration(ComposeFoundationRenderer, CatalogValidator.fromJsonString(foundationCatalogJson)),
        ),
        validationMode = ValidationMode.STRICT,
    )
}
// The host does not read the transport itself; without this loop the surface stays empty.
LaunchedEffect(host, transport) { transport.incoming.collect { host.apply(it) } }

SduiSurface(host = host)

Loading and error slots

Screen.kt
SduiSurface(
    state = state,
    renderer = FdsRenderer,
    errorComponent = { node, error -> MyErrorTile(node.id, error) },
    loadingComponent = { componentId -> MySkeleton() },
)

Intercept actions

An interceptor sees each action before it is sent. It returns the action, a changed copy, or null to consume it. Use it for analytics, confirmation dialogs, or closing a sheet locally.

Screen.kt
val host = SurfaceHost(
    transport = transport,
    actionInterceptors = listOf(ActionInterceptor { action -> analytics.track(action.name); action }),
)

Without a server

The runtime includes a FixtureTransport that replays a list of messages. Use it for previews, tests and offline demos.

Subtree theming

A container component can theme a subtree of components separately from the surface theme: scope.WithTheme(theme) { themed -> themed.RenderChild(childId) } merges the wrapper theme over the outer theme (tokens, mode, overrides) for all children below it.

MyDarkCard.kt
@Composable
fun MyDarkCard(scope: RenderScope, node: SduiNode) {
    val darkCardTheme = ThemeSpec(mode = ThemeMode.DARK)
    scope.WithTheme(darkCardTheme) { themed ->
        themed.RenderChildren(node, "children")
    }
}

Testing catalog components

dev.fediim:anysdui-compose-testing tests catalog components on a real Compose surface inside runComposeUiTest. An SduiTestController creates surfaces, applies updates, stubs components by id or component name, and asserts on actions and host errors.

MyComponentTest.kt
@OptIn(ExperimentalTestApi::class)
@Test
fun testButtonClick() = runComposeUiTest {
    val controller = SduiTestController(listOf(CatalogRegistration(FdsRenderer, CatalogValidator.fromJsonString(fdsCatalogJson))))
    controller.createSurface(
        components = listOf(rootBox, buttonNode),
        dataModel = buildJsonObject { put("count", 0) },
    )
    setContent { SduiTestSurface(controller) }
    onNodeWithText("Increment").performClick()
    assertEquals("incrementAction", controller.actions.single().name)
    controller.close()
}

Drive from a server

Any server that sends the messages above over a WebSocket can drive a surface. For a JVM server, anysdui-protocol gives you the message types and anysdui-validation checks a message against a catalog before you send it. The repository contains a reference Ktor server; it is a sample, not a published library.

MyScenario.kt · reference server
class MyScenario(private val catalog: Catalog) : ScenarioHandler {

    override val requiredCatalogIds: List<String> = listOf(catalog.catalogId)

    override suspend fun onConnect(session: SduiSession) {
        session.send(catalog.createSurface("my_dynamic_screen", sendDataModel = false) {
            component("DsText", id = "welcome_text") { prop("text", "Welcome to AnySDUI") }
        })
    }

    override suspend fun onAction(session: SduiSession, action: Action) {
        when (action.name) {
            "buttonClicked" -> {
                val userName = action.context["/inputs/userName"]?.jsonPrimitive?.content ?: ""
                session.send(UpdateDataModel(
                    surfaceId = action.surfaceId,
                    path = "/status/message",
                    value = JsonPrimitive("Hello, $userName!"),
                ))
            }
        }
    }
}

fun Application.module() {
    sduiModule(scenarioHandler = MyScenario(catalog))
}

Multi-screen flows & navigation (RFC-0012)

Rooms hold complete multi-screen flows with presenter tracking and independent browsing. A client sends the standard anysdui.navigate action with context targetFrameId or mode ("follow" or "explore"). In Follow mode, paired devices follow live editor and presenter changes. In Explore mode, a user browses the flow independently without interrupting other devices.

Push API and signed share links

External tools like the Figma plugin push entire flows in one atomic call to POST /playground/push using server-issued push tokens (Authorization: Bearer asp_…). The server expands composite components, uploads images with SHA-256 deduplication, and can issue HMAC-SHA256 signed share links (/view/{viewId}?exp=…&sig=…) that let stakeholders view and navigate flows in any browser with zero installation.

Render an A2A agent instead

With anysdui-a2a, the same host renders any agent that speaks A2A 0.3 or 1.0 and emits A2UI.

AgentScreen.kt
val session = A2aAgentConnection("https://agent.example.com", catalogIds, engine).connect()
val transport = A2aTransport(session, dataModel = ::sendDataModelSurfaces)
val host = SurfaceHost(registrations, transport,
    validationMode = ValidationMode.STRICT, limits = SurfaceHostLimits.A2A) // one host per session
transport.incoming.collect { host.apply(it) }

You pass the Ktor engine. It must not follow redirects, keep cookies or add credentials of its own.

Agents

Kotlin agent SDK

dev.fediim:anysdui-agent sits between your model client and the renderer. It does not call a model and has no transport. It gives you three things: the catalogs narrowed to what the renderer supports, a prompt snippet for your system prompt, and a parser that returns only messages the renderer would accept.

Agent.kt
// Once per agent: immutable and safe to share between sessions.
val catalog: RcCatalog = InMemoryCatalogProvider(Json.parseToJsonElement(catalogJson) as JsonObject).load()
val generator = A2uiGenerator(catalogs = listOf(CatalogConfig(catalog)))

// Once per session with one renderer. Keep it for every later turn.
val session: A2uiRequestProcessor =
    generator.createProcessor(A2uiRendererCapabilities.fromJson(capabilitiesJson, AgentLimits()))

// Each turn
val systemPrompt = "You are a helpful assistant.\n\n" + session.promptSnippet
val parts: List<ResponsePart> = session.parseResponse(callModel(systemPrompt))
// TextPart = prose for the user. A2uiPart = checked messages; send them to the renderer in order.
  • parseResponse throws A2uiParseException or A2uiValidationException instead of returning a partial result. The validation error carries hints you can feed back to the model for a repair turn.
  • Streaming is supported: feed chunks as they arrive and receive a part when its block closes.
  • It runs on Android, JVM, JS, WasmJs and iOS, and brings neither Compose nor Ktor.

Agent skills

The repository holds one generated skill per catalog under contract/skills/, plus a shared a2ui-core. Copy a2ui-core and the folder for each catalog you target, keeping the folder names.

FrameworkWhere the folders go
Claude Code.claude/skills/
Gemini CLI.gemini/skills/ or .agents/skills/
ADK (Python)Load each with load_skill_from_dir and pass them to SkillToolset

The server as an agent

The reference server can serve A2A 1.0 itself, with bearer-token authentication and a mandatory daily spending cap. Its AI helper writes layouts with Claude, Gemini, OpenAI (using the Responses API with reasoning effort selection), or local models via Ollama. It validates each result and repairs errors before anything reaches a device. The repository includes an end-to-end design-system chatbot tutorial and sample.

Theming & motion

The host app sets a base theme and decides how far an agent may change it.

Screen.kt
val host = SurfaceHost(
    registrations = registrations,
    hostTheme = ThemeSpec(
        colors = mapOf("primary" to ColorToken(light = "#6750A4", dark = "#D0BCFF")),
        motion = ThemeMotion(
            durations = mapOf("short" to 150, "medium" to 300),
            easings = mapOf("standard" to CubicBezierEasing(0.2f, 0f, 0f, 1f)),
        ),
    ),
    themePolicy = ThemePolicy.MERGE, // the default; also HOST_ONLY, AGENT_WINS
)
  • A ThemeSpec holds mode, token maps (colors, typography, fontFamilies, shapes, spacing, elevation), motion tokens, state-layer opacities and per-component overrides with defaults, variants and states.
  • Motion tokens: durations, easings, springs, and transitions define how state transitions and child enter/exit in lists animate.
  • State transitions: The generated renderer automatically animates color, Dp and Float parameter changes across states (e.g. hovered, pressed, focused) using scope.animatedColor, animatedDp, and animatedFloat.
  • Enter and exit: Children of Column and Row animate when appearing or disappearing according to component enter/exit tokens.
  • Reduced motion: The runtime detects system accessibility settings and enforces a MotionPolicy (spatial movements snap immediately while color transitions adjust gracefully).
  • Subtree theming: RenderScope.WithTheme(theme) lets a container component override tokens, component definitions and mode (e.g. rendering a dark card inside a light surface) for its subtree without affecting the rest of the surface.
  • Third-party catalogs declare their own token sets in anysdui_types.<slug>.json and catalog.bundle.json; built-in token sets are matched by exact catalog id.
  • Only colours carry separate light and dark values. Any token can alias another with {"ref": "primary"}.
  • An agent's first theme rides in createSurface.metadata.extensions.anysdui_theme. Later it calls setTheme or setThemeMode.
  • The app can change a surface directly with SurfaceState.updateTheme and updateMode; the policy does not restrict those.

Import design tokens

terminal · in a checkout of anysdui-kmp
# W3C Design Tokens: modes come as two token sets; --dark is optional.
./gradlew :anysdui-theme-import:run --args="dtcg --light tokens/light.tokens.json --dark tokens/dark.tokens.json --mapping tokens/mapping.json --out build/theme.json"

The result is a ThemeSpec JSON file, validated with the same rules the runtime applies. Exports from Figma, Tokens Studio and Style Dictionary are supported, including duration, cubicBezier and transition motion tokens.

Security and limits

A server-driven screen is untrusted input. The runtime treats it that way.

AreaWhat the runtime does
ValidationEvery message is checked against the catalog that will render it. STRICT rejects what does not match; LENIENT drops the unusable part and draws the rest. Renderer and validator are registered together, so they cannot disagree.
User activationFunctions such as openUrl run only from a callback marked @SduiUserGesture, never from composition, a timer or a load callback.
ImagesImage URLs load only from origins the app allows. The list is empty by default.
3D modelsOrigin allowlist, a fail-closed size manifest on the server, a byte ceiling and redirect pinning on the client.
ThemesAt most 65,536 characters, 256 tokens per kind, 8 alias hops, 64 component entries. Font families come from an allowlist, never from URLs.
Charts & CupertinoBounds on data arrays (up to 12 series, 500 points, 50 pie slices; at most 6 segments), numbers clamped to finite ranges, string labels cut to 64 chars.
Push API & imagesPush routes require bearer tokens (asp_…). Uploaded images verified by magic bytes (PNG, JPEG, WebP) with SHA-256 integrity, size ceilings, and served with Content-Security-Policy: sandbox.
Composite expansionComposites nest at most 4 deep, with cycle detection, linear graph walking, and a 100 composite instance cap per push.
Share linksHMAC-SHA256 signed, time-limited (up to 30 days), no server-side secret leakage, pinned room caps, and viewer limits (up to 256 per room).
A2ABounded event, line, body and message sizes while reading. Only the configured origin is fetched. Plain http needs an explicit loopback flag.
Agent-only functionssetTheme, setThemeMode and the model3d_* camera functions can be called by the agent, not from a component.

Run the repository

To try the playground, the sample server and the demo apps, clone fediim-dev/anysdui-kmp. A root Makefile wraps every task; make help lists them and make doctor reports the tools it finds.

CommandWhat it does
make run-serverStarts the server on port 8081
make run-web-wasmStarts the web app and the visual playground editor
make run-desktopRuns the desktop companion app
make run-androidInstalls the Android debug app
make build-ios-simBuilds the iOS simulator framework; then run iosApp from Xcode
make test-ciRuns the test tasks CI runs
make test-a2ui-conformanceRuns upstream's A2UI v1.0 conformance cases
make verifyGuards, CI tests and conformance together

To pair a device with the playground, point a client at ws://localhost:8081/a2ui?scenario=playground&pairingCode=<code>, using the code shown in the editor's top bar.

Known limitations & platform status

  • Pre-1.0. No binary-compatibility promise between releases yet.
  • Not on Maven Central. Published to GitHub Packages (group dev.fediim); downloads need a token with read:packages scope.
  • Package names. Renamed to dev.fediim.anysdui across all modules and published artifacts.
  • Android floor. Requires Android minSdk 28 (API 28 is the runtime verification floor).
  • Material 3. anysdui-compose-material3 implements 60 components across content, navigation and overlay tiers, and depends on Material3 1.12.0-alpha03.
  • Media. Video and AudioPlayer in the basic catalog draw labelled placeholders.
  • Desktop 3D. Model3D draws a placeholder on Desktop (Foundation, Basic, Material 2, Material 3, Cupertino, and Charts catalogs run natively on all 6 targets with no placeholders).
  • Native iOS / SwiftUI spike. A native SwiftUI renderer was evaluated in #1283 and parked; Compose Multiplatform remains the single cross-platform rendering engine for full fidelity.
  • A2UI coverage. 400 of 409 applicable upstream v1.0 RC conformance cases pass in CI with @path and @call support; the other 9 are documented differences, each with its issue. A2UI 0.9.x and compact output formats are not supported.
  • iOS publishing. The iOS artifacts can only be built on macOS.