kiliczsh/genui-kit
A SwiftUI renderer for the A2UI protocol.
Why A2UI
A2UI specifies a production-track architecture for generative UI: component catalog as JSON Schema, flat component lists with ID-reference adjacency (LLM-friendly, streaming-safe), a small closed data-binding vocabulary (Dynamic* types: literal, JSON Pointer path, or named function call — no custom expression parser needed), and a formal action/function-call protocol with client/server execution-boundary security (callableFrom: clientOnly | remoteOnly). Adopting the standard buys interoperability with any A2UI-speaking agent backend (via AG-UI, A2A, or MCP transport), not just one specific SDK.
Core concept (A2UI's, implemented here for SwiftUI)
- Catalog — a JSON Schema describing which component types, their
props, and which functions/actions a surface may reference. The host app supplies its own catalog (or the A2UI "basic catalog"); an agent can only reference what's in it.
- Envelope stream — the agent sends a stream of messages:
createSurface, updateComponents, updateDataModel, deleteSurface, plus the bidirectional action/actionResponse and callFunction/functionResponse pair.
- Surface — a named UI region with its own component tree (flat
list, ID-referenced) and data model (plain JSON, updated via JSON Pointer paths).
- Renderer — a SwiftUI
Viewthat consumes the envelope stream,
maintains surface/component/data-model state, and resolves each catalog component reference to a real View via a host-supplied registry.
Requirements
- macOS 14+ / iOS 17+ / visionOS 1+
- Swift 5.9+
- No external package dependencies
Installation
.package(url: "https://github.com/kiliczsh/genui-kit.git", from: "0.1.0")Two library products: GenUIKit (core protocol + renderer) and GenUIKitBasicCatalog (SwiftUI implementations of the A2UI basic catalog; depends on GenUIKit).
Quick start
import SwiftUI
import GenUIKit
import GenUIKitBasicCatalog
let store = SurfaceStore()
let registry = BasicCatalog.registry()
BasicCatalog.registerStandardFunctions(into: store.functions)
// Feed envelopes from anywhere: a live agent stream, a fixture file,
// or a hand-written test message.
try store.apply(envelope: createSurfaceEnvelope)
// Render every active surface:
ForEach(store.orderedSurfaces, id: \.id) { surface in
SurfaceRenderer(surface: surface, registry: registry, store: store)
}SurfaceStore.ingest(_:) consumes any AsyncSequence of envelopes for streaming use; store.emitAction(...) and two-way input bindings cover the client→agent direction.
Local actions and reactive values
Deterministic interactions can stay entirely on-device. SurfaceStore ships an allowlisted LocalActionRegistry containing set, toggle, increment, append, remove, filter, sort, and applyForm:
{
"action": {
"localAction": {
"name": "increment",
"args": {"path": "/travelers", "by": 1}
}
}
}localAction never emits on store.outbound; agentAction does. The standard A2UI spellings remain compatible: functionCall is a read-only client function and event is an outbound agent action. Host code may allowlist more Swift handlers with store.localActions.register(...); agent JSON can only invoke registered names and only receives declarative JSON arguments.
Value-returning actions accept resultPath. resultPaths can fan out members of an object result to several data-model paths. Every component bound to one of those paths refreshes through Observation.
Reactive values can be declared on a surface and are materialized after creation and every subsequent data-model write:
{
"surfaceProperties": {
"computedValues": [{
"resultPath": "/budget/remaining",
"call": "budgetRemaining",
"args": {
"budget": {"path": "/budget/total"},
"spent": {"path": "/budget/spent"}
}
}]
}
}Built-in computed functions cover sum, subtract, average, percentage, remaining/budgetRemaining, weighted score, compare, formValid, dateAdd, and dateDifference.
Project independence
This package has no dependency on genui-app, claude-agents-sdk-swift, or any particular agent/LLM backend. It consumes an A2UI message stream from wherever one comes from — a live agent connection, a static fixture file, or a hand-written test envelope. Translating a specific agent SDK's output (e.g. Claude Agent SDK tool calls) into A2UI envelope messages is a separate concern, handled by an adapter package (genui-bridge), not by this one.
Status
Implemented against A2UI v1.0 (release candidate) and verified by 56 tests including image-rendering smoke tests — no AI/network dependency anywhere in the suite:
GenUIKit(core): envelope decoding for all six server→client
message kinds (+ forward-compatible unknown preservation), surface store (@MainActor/@Observable), JSON Pointer data model with two-way input write-back, catalog loading with structural validation, the full Dynamic* binding vocabulary including catalog functions and @index, template-list children, outbound action/functionResponse /error emission, callableFrom boundary enforcement, and AsyncSequence envelope ingestion. A backward-compatible GenUI action extension adds explicit local/agent routing, allowlisted deterministic mutations, result-path writeback, and materialized reactive values.
GenUIKitBasicCatalog(additive): SwiftUI implementations of all
18 basic-catalog components, reactive checks, and all 14 catalog functions (@index is supplied by the core as a system function).
The A2UI schemas this was verified against are vendored under spec/a2ui/ with pinned provenance. The exact researched coverage and intentional renderer decisions are in docs/A2UI_V1_COMPATIBILITY.md. DocC documentation and an example app are planned.
Package Metadata
Repository: kiliczsh/genui-kit
Default branch: main
README: README.md