A compact syntax for LLMs to generate native mobile UI on the fly
AME is a line-oriented markup that LLMs stream token-by-token. A mobile renderer turns each line into a native component the moment it arrives — no JSON parsing, no WebView, no round-trip.
An LLM returns AME text instead of plain prose when the user needs a rich, interactive answer. Each line defines one UI element — the renderer builds the screen progressively as tokens arrive.
LLM output (8 lines, 188 tokens):
root = col([title, form, submit_btn])
title = txt("Book a Table", headline)
form = col([name_in, date_in, size_in, note_in])
name_in = input("name", "Your name")
date_in = input("date", "Date")
size_in = input("party_size", "Party size")
note_in = input("notes", "Special requests")
submit_btn = btn("Reserve", submit(book_table))
What the user sees: a native Material 3 form with four text inputs and a "Reserve" button. Tapping the button collects form values and routes them through the host app's tool-call pipeline — same confirmation flow as any other LLM-initiated action.
-
Compact. 1.77x fewer tokens than comparable UI syntaxes on average. Every token saved reduces latency and cost at scale. Benchmark →
-
Streaming-first. Line-oriented syntax enables progressive rendering — skeleton placeholders appear instantly, content fills in as each line arrives. First visible content in ~0.10s. Benchmark →
-
LLM-reliable. Zero parse failures across 32 prompts on both Gemini 3 Flash Preview and Claude Sonnet 4.6 with the standard system prompt. Benchmark →
-
Native mobile. Jetpack Compose and SwiftUI renderers. Material 3 theming on Android, SF Symbols and system fonts on iOS. No WebView.
-
Action safety. Button taps route through the host app's trust pipeline — same confirmation and risk system as LLM-initiated tool calls. The renderer never executes actions directly.
-
Zero-token templates. The majority of tool-result interactions render as rich native UI through shape-matched templates — no LLM tokens needed for UI generation. The LLM only produces AME for novel or complex layouts. Spec →
// 1. Parse AME text into a node tree
val parser = AmeParser()
val tree = parser.parse(ameString)
// 2. Render with Compose
tree?.let {
AmeRenderer(
node = it,
onAction = { action -> handleAction(action) }
)
}
// 3. Handle actions from the host app
fun handleAction(action: AmeAction) {
when (action) {
is AmeAction.CallTool -> toolSystem.execute(action.name, action.args)
is AmeAction.OpenUri -> startActivity(Intent(ACTION_VIEW, Uri.parse(action.uri)))
is AmeAction.Navigate -> navController.navigate(action.route)
is AmeAction.CopyText -> clipboard.setText(action.text)
is AmeAction.Submit -> { /* Resolved to CallTool by renderer before dispatch */ }
}
}
// 4. Customize theme (optional)
CompositionLocalProvider(LocalAmeTheme provides myThemeConfig) {
AmeRenderer(node = tree, onAction = ::handleAction)
}| Dimension | AME | A2UI v0.9 | Raw JSON | MCP Apps | OpenUI Lang |
|---|---|---|---|---|---|
| Token cost (avg) | 1x (baseline) | 1.77x | ~0.87x † | N/A (HTML) | ~0.95x * |
| Streaming | Line-by-line | Flat list | Not streamable | Pre-built | Line-by-line |
| Zero-token rendering | Yes | No | No | No | No |
| Mobile-native renderer | Compose + SwiftUI | Flutter | Custom code | WebView | React |
| Typed AST + error recovery | Yes | No | No | No | No |
| Action safety | Host trust pipeline | Event-based | N/A | iframe sandbox | Callbacks |
* OpenUI Lang token cost is estimated from syntax comparison, not measured via tokenizer. All other AME and A2UI numbers are measured.
† Raw JSON is ~13% fewer tokens than AME on average, but is not incrementally parseable (no streaming), has no typed AST or error recovery, and requires custom rendering code per schema. AME's overhead pays for streaming progressiveness, a typed node tree with error recovery, and platform-native rendering from a single format.
AME is an independent open-source project. Comparisons are based on public documentation and measured token counts.
The complete AME v1.1 specification: specification/v1.0/
| Document | Description |
|---|---|
| syntax.md | Line-oriented syntax rules and EBNF grammar |
| primitives.md | 21 standard UI primitives with argument tables |
| actions.md | 5 action types (tool, uri, nav, copy, submit) |
| streaming.md | Progressive rendering with forward references |
| data-binding.md | $path references, --- separator, each() templates |
| tier-zero.md | Zero-token tool-driven rendering |
| integration.md | Host app capability declaration and system prompt guidance |
Measured with Gemini gemini-2.0-flash tokenizer. Every number below is a
measured integer from the countTokens API — no estimates, no rounding.
| UI Scenario | AME | A2UI v0.9 | Savings |
|---|---|---|---|
| Weather Card | 131 | 203 | 1.55x |
| Place Search (3 cards) | 581 | 1,014 | 1.75x |
| Email Inbox (5 items) | 420 | 605 | 1.44x |
| Booking Form (4 inputs) | 188 | 412 | 2.19x |
| Comparison (2 cards) | 604 | 889 | 1.47x |
| Medical Dashboard (chart + timeline) | 218 | 743 | 3.41x |
| Code Tutorial (code + accordion) | 294 | 554 | 1.88x |
| Product Gallery (carousel + chart) | 436 | 664 | 1.52x |
| Average | 359 | 636 | 1.77x |
Full benchmark with methodology and reproducible strings →
First visible content in ~0.10s vs ~10.1s for batch-rendered JSON alternatives (simulated at 100 tokens/s). A2UI streaming JSON parsers can narrow the first-content gap to ~0.5s, but AME retains structural advantages (skeleton placeholders, simpler parser, 1.75x fewer tokens). Full benchmark →
Zero parse failures across 32 prompts (20 v1.0 + 12 v1.1) on both Gemini 3 Flash Preview and Claude Sonnet 4.6. Full benchmark →
ame-spec/
├── specification/v1.0/ # 7 spec documents (syntax, primitives, actions, streaming, data-binding, tier-zero, integration)
├── ame-core/ # Kotlin data model + parser (AmeNode, AmeAction, AmeParser, AmeSerializer)
├── ame-compose/ # Jetpack Compose renderer (AmeRenderer, AmeTheme, AmeFormState, AmeIcons, AmeChartRenderer)
├── ame-swiftui/ # SwiftUI renderer (AmeRenderer, AmeTheme, AmeFormState, AmeIcons)
├── conformance/ # 55 conformance tests with Kotlin-Swift parity check
├── benchmarks/ # Token comparison, streaming latency, LLM reliability
└── examples/ # 9 standalone .ame files
The following limitations are known and planned for future versions:
-
Single tokenizer benchmark. All token measurements in token-comparison.md use the Gemini
gemini-2.0-flashtokenizer. GPT-4o and Claude tokenizers have different vocabulary tables and BPE merge rules. The 1.77x ratio is specific to the Gemini tokenizer and may vary on other tokenizers. Community contributions of cross-tokenizer benchmarks are welcome. -
Grammar disambiguation. The EBNF grammar in syntax.md relies on a prose disambiguation rule for
row()'s second positional argument (numeric literal =gap, enum identifier =align). This rule is specified in primitives.md but is not expressible in EBNF alone. Parser implementors MUST read therowsection in primitives.md for the complete disambiguation rule. -
Custom component catalog schema. The spec defines how to declare custom components in
AME_CATALOG(integration.md) but does not define a schema format for describing custom component parameters to agents. Host apps document their custom components in their system prompts. A formal custom catalog schema is planned for a future version. -
Two independent parsers. The reference implementation includes independent Kotlin and Swift parsers that must produce identical output. Parser parity is enforced by the conformance test suite (
conformance/check-parity.sh) with 55 test cases covering all 21 primitives. Both parsers implementeach()expansion at parse time — in streaming mode,each()nodes show a shimmer placeholder until the data section arrives.
Apache 2.0 — see LICENSE