Contents

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)

  1. 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.

  1. Envelope stream — the agent sends a stream of messages:

createSurface, updateComponents, updateDataModel, deleteSurface, plus the bidirectional action/actionResponse and callFunction/functionResponse pair.

  1. Surface — a named UI region with its own component tree (flat

list, ID-referenced) and data model (plain JSON, updated via JSON Pointer paths).

  1. Renderer — a SwiftUI View that 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