From 8dc92bf9bd9dba593dbe13d5bbdbf48a88b40292 Mon Sep 17 00:00:00 2001 From: Jacob Simionato Date: Fri, 6 Mar 2026 07:24:36 +1030 Subject: [PATCH 01/13] Add resources discussion --- specification/v0_9/docs/renderer_guide.md | 24 +++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/specification/v0_9/docs/renderer_guide.md b/specification/v0_9/docs/renderer_guide.md index 7bc331b32..13bfc9568 100644 --- a/specification/v0_9/docs/renderer_guide.md +++ b/specification/v0_9/docs/renderer_guide.md @@ -411,3 +411,27 @@ The standard `formatString` function is responsible for interpreting the `${expr * **FunctionCall**: Identified by parentheses (e.g., `${now()}`). 3. **Escaping**: Literal `${` sequences must be handled (typically by escaping as `\${`). 4. **Reactive Coercion**: Results are transformed into strings using the **Type Coercion Standards** defined in the Data Layer section. + +## Resources + +When implementing a new rendering framework, you should definitely read the core JSON schema files for the protocol and the markdown doc in the specification. Here are the key resources: + +* **A2UI Protocol Specification:** + * `specification/v0_9/docs/a2ui_protocol.md` + * [GitHub Link](https://github.com/google/A2UI/tree/main/specification/v0_9/docs/a2ui_protocol.md) +* **JSON Schemas:** (Core files for the protocol) + * `a2ui_client_capabilities.json`: [`specification/v0_9/json/a2ui_client_capabilities.json`](https://github.com/google/A2UI/tree/main/specification/v0_9/json/a2ui_client_capabilities.json) + * `a2ui_client_data_model.json`: [`specification/v0_9/json/a2ui_client_data_model.json`](https://github.com/google/A2UI/tree/main/specification/v0_9/json/a2ui_client_data_model.json) + * `basic_catalog.json`: [`specification/v0_9/json/basic_catalog.json`](https://github.com/google/A2UI/tree/main/specification/v0_9/json/basic_catalog.json) + * `basic_catalog_rules.txt`: [`specification/v0_9/json/basic_catalog_rules.txt`](https://github.com/google/A2UI/tree/main/specification/v0_9/json/basic_catalog_rules.txt) + * `client_to_server.json`: [`specification/v0_9/json/client_to_server.json`](https://github.com/google/A2UI/tree/main/specification/v0_9/json/client_to_server.json) + * `client_to_server_list.json`: [`specification/v0_9/json/client_to_server_list.json`](https://github.com/google/A2UI/tree/main/specification/v0_9/json/client_to_server_list.json) + * `common_types.json`: [`specification/v0_9/json/common_types.json`](https://github.com/google/A2UI/tree/main/specification/v0_9/json/common_types.json) + * `server_to_client.json`: [`specification/v0_9/json/server_to_client.json`](https://github.com/google/A2UI/tree/main/specification/v0_9/json/server_to_client.json) + * `server_to_client_list.json`: [`specification/v0_9/json/server_to_client_list.json`](https://github.com/google/A2UI/tree/main/specification/v0_9/json/server_to_client_list.json) +* **Web Core Reference Implementation:** + * `renderers/web_core/src/v0_9/` + * [GitHub Link](https://github.com/google/A2UI/tree/main/renderers/web_core/src/v0_9) +* **Flutter Implementation:** + * The Flutter renderer is maintained in a separate repository. + * [GitHub Link](https://github.com/flutter/genui/tree/main/packages/genui) From c5a8eb32cc0bc6c2bc4cf6659acae050454106c7 Mon Sep 17 00:00:00 2001 From: Jacob Simionato Date: Fri, 6 Mar 2026 07:46:38 +1030 Subject: [PATCH 02/13] Improve functions design --- specification/v0_9/docs/renderer_guide.md | 94 +++++++++++------------ 1 file changed, 45 insertions(+), 49 deletions(-) diff --git a/specification/v0_9/docs/renderer_guide.md b/specification/v0_9/docs/renderer_guide.md index 13bfc9568..7697b67ec 100644 --- a/specification/v0_9/docs/renderer_guide.md +++ b/specification/v0_9/docs/renderer_guide.md @@ -12,6 +12,27 @@ The Data Layer is responsible for receiving the wire protocol (JSON messages), p It consists of three sub-components: the Processing Layer, the Dumb Models, and the Context Layer. +### Prerequisites + +To implement the Data Layer effectively, your target environment needs two foundational utilities: a Schema Library and an Observable Library. + +#### 1. Schema Library +To represent and validate component and function APIs, the Data Layer requires a **Schema Library**. + +* **Ideal Choice**: A library (like **Zod** in TypeScript or **JsonSchemaBuilder** in Flutter) that allows for programmatic definition of schemas and the ability to validate raw JSON data against those definitions. +* **Capabilities Generation**: The library should ideally support exporting these programmatic definitions to standard JSON Schema for the `getClientCapabilities` payload. +* **Fallback**: If no suitable programmatic library exists for the target language, raw **JSON Schema strings** or manual validation logic can be used instead. + +#### 2. Observable Library +A2UI relies on a standard observer pattern to reactively update the UI when data changes. The Data Layer and client-side functions must be able to return streams or reactive variables that hold an initial value and emit subsequent updates. + +* **Requirement**: You need a reactive mechanism that acts like a "BehaviorSubject" or a stateful stream—it must have a current value available synchronously upon subscription, and notify listeners of future changes. +* **Examples by Platform**: + * **Web (TypeScript/JavaScript)**: RxJS (`BehaviorSubject`), Signals, or a simple custom `EventEmitter` class. + * **Android (Kotlin)**: Kotlin Coroutines (`StateFlow`) or Android `LiveData`. + * **iOS (Swift)**: Combine (`CurrentValueSubject`) or SwiftUI `@Published` / `Binding`. +* **Guidance**: If your ecosystem doesn't have a lightweight built-in option, you can easily implement a simple observer class with `subscribe` and `unsubscribe` methods, keeping external dependencies low. + ### Design Principles To ensure consistency and portability, the Data Layer implementation relies on standard patterns rather than framework-specific libraries. @@ -47,13 +68,6 @@ The model is designed to support high-performance rendering through granular upd This hierarchy allows a renderer to implement "smart" updates: re-rendering a container only when its children list changes, but updating just a specific text node when its bound data value changes. -### Schema Library Requirements -To represent and validate component and function APIs, the Data Layer requires a **Schema Library**. - -* **Ideal Choice**: A library (like **Zod** in TypeScript or **JsonSchemaBuilder** in Flutter) that allows for programmatic definition of schemas and the ability to validate raw JSON data against those definitions. -* **Capabilities Generation**: The library should ideally support exporting these programmatic definitions to standard JSON Schema for the `getClientCapabilities` payload. -* **Fallback**: If no suitable programmatic library exists for the target language, raw **JSON Schema strings** or manual validation logic can be used instead. - ### Key Interfaces and Classes * **`MessageProcessor`**: The entry point that ingests raw JSON streams. * **`SurfaceGroupModel`**: The root container for all active surfaces. @@ -61,7 +75,7 @@ To represent and validate component and function APIs, the Data Layer requires a * **`SurfaceComponentsModel`**: A flat collection of component configurations. * **`ComponentModel`**: A specific component's raw configuration. * **`DataModel`**: A dedicated store for application data. -* **`DataContext`**: A scoped window into the `DataModel`. +* **`DataContext`**: A scoped window into the `DataModel`. Used by functions and components to resolve dependencies and mutate state. * **`ComponentContext`**: A binding object pairing a component with its data scope. ### The Catalog and Component API @@ -73,61 +87,44 @@ interface ComponentApi { readonly schema: z.ZodType; // Technical definition for capabilities } -/** - * Context provided to functions during execution. - * Allows functions to resolve other dynamic values or interact with the data model. - */ -interface FunctionContext { - /** The current data model path context (useful for relative paths). */ - readonly path: string; - - /** - * Resolves any DynamicValue (literal, path, or function call) to a reactive stream. - * The returned type should be an observable/listenable implementation idiomatic to the language. - */ - resolve(value: any): Observable; - - /** Retrieves another registered function by name. */ - getFunction(name: string): ClientFunction | undefined; - - /** Updates the data model at a specific path. */ - update(path: string, value: any): void; -} - /** * Defines a client-side logic handler. */ -interface ClientFunction { +interface FunctionImplementation { readonly name: string; readonly description: string; readonly returnType: 'string' | 'number' | 'boolean' | 'array' | 'object' | 'any' | 'void'; /** - * The schema for the arguments this function accepts (similar to Flutter's `argumentSchema`). + * The schema for the arguments this function accepts. * MUST use the same schema library as the ComponentApi to ensure consistency - * across the catalog. + * across the catalog. + * This maps directly to the `parameters` field of the `FunctionDefinition` + * in the A2UI client capabilities schema, allowing dynamic capabilities advertising. */ - readonly argumentSchema: z.ZodType; + readonly schema: z.ZodType; /** * Executes the function logic. * @param args The key-value pairs of arguments provided in the JSON. - * @param context The execution context for resolving dependencies. - * @returns A reactive stream (or Observable/Signal) of the result. + * @param context The DataContext for resolving dependencies and mutating state. + * @returns A synchronous value or a reactive stream (e.g. Observable). * - * Rationale: Like the Model Layer, functions MUST return an observable implementation - * that is idiomatic to the target language but follows "lowest common denominator" - * principles: low dependency, multi-cast support, and a standard unsubscription pattern. + * Rationale: The return type here should be flexible based on your language. + * Dynamic languages (like TS/JS) can return a union type (e.g., `unknown | Observable`) + * and let the framework wrap static values. Strictly typed languages (like Swift or Kotlin) + * might instead require this to strictly return their observable equivalent (e.g., `StateFlow`) + * internally wrapping static returns to avoid messy generic union types. */ - execute(args: Record, context: FunctionContext): Observable; + execute(args: Record, context: DataContext): unknown | Observable; } class Catalog { readonly id: string; // Unique catalog URI readonly components: ReadonlyMap; - readonly functions: ReadonlyMap; + readonly functions?: ReadonlyMap; - constructor(id: string, components: T[], functions: ClientFunction[] = []) { + constructor(id: string, components: T[], functions?: FunctionImplementation[]) { // Initializes the read-only maps } } @@ -136,12 +133,12 @@ class Catalog { #### Function Implementation Rationale A2UI categorizes client-side functions to balance performance and reactivity. -**Observability Consistency**: Like the "Dumb Models," functions MUST use a listening mechanism (streams, callbacks, or listenable properties) that is idiomatic to the language but follows "lowest common denominator" principles: low dependency, multi-cast support, and a standard unsubscription pattern. +**Observability Consistency**: Functions can return either a synchronous literal value (for static results) or a reactive stream (for values that change over time). The execution engine (`DataContext`) is responsible for treating these consistently by wrapping synchronous returns in static observables when evaluating reactively. -**API Documentation**: Every function MUST include a schema (e.g., `argumentSchema`) using the same schema library selected for the Data Layer. This allows the renderer to validate function arguments at runtime and generate accurate client capabilities for the AI model. +**API Documentation**: Every function MUST include a `schema` using the same schema library selected for the Data Layer. This allows the renderer to validate function arguments at runtime and generate accurate client capabilities (`parameters` in `FunctionDefinition`) for the AI model. **Function Categories**: -1. **Pure Logic (Synchronous)**: Functions like `add` or `concat`. While they return observable streams for consistency, their logic is immediate and depends only on their inputs. +1. **Pure Logic (Synchronous)**: Functions like `add` or `concat`. Their logic is immediate and depends only on their inputs. They typically return a static primitive value. 2. **External State (Reactive)**: Functions like `clock()` or `networkStatus()`. These return long-lived streams that push updates to the UI independently of data model changes. 3. **Effect Functions**: Side-effect handlers (e.g., `openUrl`, `closeModal`) that return `void`. These are typically triggered by user actions rather than interpolation. @@ -339,6 +336,7 @@ class ComponentContext { readonly surfaceComponents: SurfaceComponentsModel; // The escape hatch dispatchAction(action: any): Promise; // Propagate action to surface } +``` #### Inter-Component Dependencies (The "Escape Hatch") While A2UI components are designed to be self-contained, certain rendering logic requires knowledge of a child or sibling's properties. @@ -348,8 +346,6 @@ While A2UI components are designed to be self-contained, certain rendering logic **Usage**: Component implementations can use `ctx.surfaceComponents` to inspect the metadata of other components in the same surface. > **Guidance**: This pattern is generally discouraged as it increases coupling. Use it only as an essential escape hatch when a framework's layout engine cannot be satisfied by explicit component properties alone. -``` - ## 2. Framework Binding Layer The Framework Binding Layer takes the structured state provided by the Data Layer and translates it into actual UI elements (DOM nodes, Flutter widgets, etc.). This layer provides framework-specific component implementations that extend the data layer's `ComponentApi` to include actual rendering logic. @@ -398,14 +394,14 @@ To ensure performance and correctness, components MUST follow these rules: The Standard A2UI Catalog (v0.9) requires a shared logic layer for expression resolution and standard component definitions. To maintain consistency across renderers, implementations should follow this structure: -* **`basic_catalog_api/`**: Contains the framework-agnostic `ComponentApi` definitions for standard components (`Text`, `Button`, `Row`, etc.) and the `ClientFunction` definitions for standard functions. +* **`basic_catalog_api/`**: Contains the framework-agnostic `ComponentApi` definitions for standard components (`Text`, `Button`, `Row`, etc.) and the `FunctionImplementation` definitions for standard functions. * **`basic_catalog_implementation/`**: Contains the framework-specific rendering logic (e.g. `SwiftUIButton`, `FlutterRow`). ### **Expression Resolution Logic (`formatString`)** The standard `formatString` function is responsible for interpreting the `${expression}` syntax within string properties. **Implementation Requirements**: -1. **Recursion**: The function implementation MUST use `FunctionContext.resolve()` to recursively evaluate nested expressions or function calls (e.g., `${formatDate(value:${/date})}`). +1. **Recursion**: The function implementation MUST use `DataContext.resolveDynamicValue()` or `DataContext.subscribeDynamicValue()` to recursively evaluate nested expressions or function calls (e.g., `${formatDate(value:${/date})}`). 2. **Tokenization**: The parser must distinguish between: * **DataPath**: A raw JSON Pointer (e.g., `${/user/name}`). * **FunctionCall**: Identified by parentheses (e.g., `${now()}`). @@ -434,4 +430,4 @@ When implementing a new rendering framework, you should definitely read the core * [GitHub Link](https://github.com/google/A2UI/tree/main/renderers/web_core/src/v0_9) * **Flutter Implementation:** * The Flutter renderer is maintained in a separate repository. - * [GitHub Link](https://github.com/flutter/genui/tree/main/packages/genui) + * [GitHub Link](https://github.com/flutter/genui/tree/main/packages/genui) \ No newline at end of file From 5df86c2b816f6fc4bf489b85307a591046cfc6da Mon Sep 17 00:00:00 2001 From: Jacob Simionato Date: Fri, 6 Mar 2026 07:49:05 +1030 Subject: [PATCH 03/13] Add comments about unsubscribing --- specification/v0_9/docs/renderer_guide.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/specification/v0_9/docs/renderer_guide.md b/specification/v0_9/docs/renderer_guide.md index 7697b67ec..7d0d629e8 100644 --- a/specification/v0_9/docs/renderer_guide.md +++ b/specification/v0_9/docs/renderer_guide.md @@ -26,7 +26,7 @@ To represent and validate component and function APIs, the Data Layer requires a #### 2. Observable Library A2UI relies on a standard observer pattern to reactively update the UI when data changes. The Data Layer and client-side functions must be able to return streams or reactive variables that hold an initial value and emit subsequent updates. -* **Requirement**: You need a reactive mechanism that acts like a "BehaviorSubject" or a stateful stream—it must have a current value available synchronously upon subscription, and notify listeners of future changes. +* **Requirement**: You need a reactive mechanism that acts like a "BehaviorSubject" or a stateful stream—it must have a current value available synchronously upon subscription, and notify listeners of future changes. Crucially, the subscription must provide a clear mechanism to **unsubscribe** (e.g., a `dispose()` method or a returned cleanup function) to prevent memory leaks when components are removed. * **Examples by Platform**: * **Web (TypeScript/JavaScript)**: RxJS (`BehaviorSubject`), Signals, or a simple custom `EventEmitter` class. * **Android (Kotlin)**: Kotlin Coroutines (`StateFlow`) or Android `LiveData`. @@ -377,6 +377,9 @@ interface MyFrameworkComponent extends ComponentApi { #### 2. Stateful / Imperative Frameworks (e.g., Vanilla DOM, Android Views) In stateful frameworks, a parent component instance might persist even as its configuration changes. In these cases, the `FrameworkComponent` might maintain a reference to its native element and provide an `update()` method to apply new properties without re-creating the entire child tree. +#### Subscription Management +Regardless of the framework paradigm (functional or stateful), the `FrameworkComponent` implementation is responsible for tracking any active subscriptions returned by the `DataContext` or `ComponentModel`. It must ensure these subscriptions are properly disposed of when the component is unmounted from the UI tree. + ### Component Traits #### Reactive Validation (`Checkable`) @@ -386,9 +389,10 @@ Interactive components that support the `checks` property should implement the ` * **Action Blocking**: Actions (like `Button` clicks) should be reactively disabled or blocked if any validation check in the surface or component fails. #### Component Subscription Lifecycle -To ensure performance and correctness, components MUST follow these rules: +To ensure performance and prevent memory leaks, components MUST strictly manage their subscriptions. Follow these rules: 1. **Lazy Subscription**: Only subscribe to data paths or property updates when the component is actually mounted/attached to the UI. -2. **Path Stability**: If a component's property (e.g., a `value` data path) changes via an `updateComponents` message, the component MUST unsubscribe from the old path and subscribe to the new one. +2. **Path Stability**: If a component's property (e.g., a `value` data path) changes via an `updateComponents` message, the component MUST unsubscribe from the old path before subscribing to the new one. +3. **Destruction / Cleanup**: When a component is removed from the UI (e.g., via a `deleteSurface` message, a conditional render, or when its parent is replaced), the framework binding MUST hook into its native lifecycle (e.g., `ngOnDestroy` in Angular, `dispose` in Flutter, `useEffect` cleanup in React, `onDisappear` in SwiftUI) to trigger unsubscription from all active data and property observables. This ensures listeners are cleared from the `DataModel`. ## **Basic Catalog Implementation** From 07077b2b9461420aa54f08f6fde795cfa517add5 Mon Sep 17 00:00:00 2001 From: Jacob Simionato Date: Fri, 6 Mar 2026 09:57:49 +1030 Subject: [PATCH 04/13] Add catalog API proposal doc v1 --- .../v0_9/docs/catalog_api_proposal.md | 284 ++++++++++++++++++ 1 file changed, 284 insertions(+) create mode 100644 specification/v0_9/docs/catalog_api_proposal.md diff --git a/specification/v0_9/docs/catalog_api_proposal.md b/specification/v0_9/docs/catalog_api_proposal.md new file mode 100644 index 000000000..97f456a56 --- /dev/null +++ b/specification/v0_9/docs/catalog_api_proposal.md @@ -0,0 +1,284 @@ +# A2UI Catalog API Architecture Proposal (v0.9) + +## Objective +To refine the A2UI client-side Catalog API to clearly separate component schema definitions, data decoding logic, and framework-specific rendering. This will improve code sharing across frameworks, enhance type safety when implementing catalogs, and provide a streamlined, idiomatic developer experience for creating custom components. + +## Requirements Addressed + +1. **Share Component Schemas**: Allow developers to declare component schemas independently so they can be reused across different platform implementations. +2. **Share Decoding Logic**: Centralize the boilerplate of resolving `DynamicValue` properties, handling reactive streams, and tracking subscriptions into a framework-agnostic "Decoder" layer. Provide details on how a generic decoder can be built using Zod for web environments. +3. **Reliable Catalog Implementation**: Provide a strongly-typed mechanism to ensure a specific framework implementation covers all required components of a defined catalog (e.g., ensuring `createAngularBasicCatalog` includes a renderer for `Button`, `Text`, etc.). +4. **Framework-Specific Adapters**: Provide idiomatic APIs for specific frameworks (e.g., a React adapter and an Angular adapter that provide standard framework props/inputs instead of raw `ComponentContext`). +5. **Streamlined DX (Stretch Goal)**: Explore a `defineCatalogImplementation` API for defining catalogs quickly with high type safety. + +--- + +## Proposed Architecture: The 4-Layer Model + +We propose breaking down the component lifecycle into four distinct layers: + +### 1. Component Schema (API Definition) +This layer defines the exact JSON footprint of a component without any rendering or decoding logic. It is the single source of truth for the component's contract, exactly mirroring the A2UI component schema definitions. + +```typescript +// basic_catalog_api/schemas.ts +export interface ComponentDefinition { + name: Name; + schema: PropsSchema; +} + +const ButtonSchema = z.object({ + label: DynamicStringSchema, + action: ActionSchema, +}); + +// Example definition +export const ButtonDef: ComponentDefinition<"Button", typeof ButtonSchema> = { + name: "Button", + schema: ButtonSchema +}; +``` + +### 2. The Decoder Layer (Framework-Agnostic) +A2UI components are heavily reliant on `DynamicValue` bindings (which resolve to reactive streams). Framework renderers currently have to manually resolve these, manage context, and handle subscriptions. The **Decoder Layer** absorbs this responsibility. + +A Decoder takes the raw `ComponentContext` and transforms the reactive A2UI bindings into a single, cohesive stream of strongly-typed `ResolvedProps`. + +```typescript +// The generic Decoder interface +export interface ComponentDecoder { + // A stream of fully resolved, ready-to-render props + readonly propsStream: Observable; + // Cleans up all underlying data model subscriptions + dispose(): void; +} + +// The Middle Layer combining Schema + Decoder Logic +export interface FrontEndLayer { + name: string; + schema: z.ZodType; + createDecoder(context: ComponentContext): ComponentDecoder; +} +``` + +#### Subscription Lifecycle and Cleanup +A critical responsibility of the Decoder is tracking all subscriptions it creates (via `context.dataContext.subscribeDynamicValue`). + +When a component is removed from the UI (because its parent was replaced, the surface was deleted, etc.), the underlying framework must call the Decoder's `dispose()` method. The Decoder then iterates through its internally tracked subscription list and calls `unsubscribe()` on each one, ensuring that no dangling listeners remain attached to the global `DataModel`. + +#### Generic Decoders via Zod (Web Implementation) +For TypeScript/Web implementations, we can write a generic `ZodDecoder` that automatically infers subscriptions. Instead of writing custom logic for every component, the decoder recursively inspects the Zod schema. + +When the `ZodDecoder` walks the schema and encounters known A2UI dynamic types (e.g., `DynamicStringSchema`), it automatically invokes `context.dataContext.subscribeDynamicValue()`. It stores the returned subscription objects in an internal array. When `dispose()` is called, it loops through this array and unsubscribes them all. + +```typescript +// Conceptual Generic Zod Decoder Factory +export function createZodDecoder( + schema: T, + context: ComponentContext +): ComponentDecoder> { + // 1. Walk the schema to find all DynamicValue and Action properties. + // 2. Map `DynamicValue` properties to `context.dataContext.subscribeDynamicValue()` + // and store the returned `DataSubscription` objects. + // 3. Map `Action` properties to `context.dispatchAction()`. + // 4. Combine all observables (e.g., using `combineLatest` in RxJS) into a single stream. + // 5. Return an object conforming to ComponentDecoder whose `dispose()` method unsubscribes all stored subscriptions. + + return new GenericZodDecoder(schema, context); +} + +// Button implementation becomes trivial: +export const ButtonFrontEnd: FrontEndLayer = { + name: "Button", + schema: ButtonDef.schema, + createDecoder: (ctx) => createZodDecoder(ButtonDef.schema, ctx) +}; +``` + +*Note for Static Languages (Swift/Kotlin):* Dynamic runtime reflection isn't as easily feasible. Swift/Kotlin environments can rely on Code Generation (Swift Macros, KSP) to generate the boilerplate `Decoder` logic based on the schema at compile-time. + +### 3. Framework-Specific Adapters +Framework developers should not interact with `ComponentContext` or `Decoder` directly when writing the actual UI view. We provide framework-specific adapters that bridge the `Decoder`'s stream to the framework's native reactivity and automatically handle the disposal lifecycle to guarantee memory safety. + +**React Adapter (Highly Reactive):** +React supports subscribing to external stores directly. The adapter leverages `useSyncExternalStore` or `useEffect` to hook into the decoder's stream, and uses the `useEffect` cleanup return function to dispose of the decoder when the component unmounts. + +```typescript +// react_adapter.ts +export function createReactComponent( + frontEnd: FrontEndLayer, + RenderComponent: React.FC +): ReactComponentRenderer { + return { + name: frontEnd.name, + schema: frontEnd.schema, + render: (ctx: ComponentContext) => { + // Adapter maps `propsStream` into React state. + // Crucially, it registers `decoder.dispose()` inside a `useEffect` cleanup block + // so when React unmounts this component, the DataModel subscriptions are severed. + return ; + } + }; +} + +// Usage: +const ReactButton = createReactComponent(ButtonFrontEnd, (props) => ( + // `label` is purely a string here +)); +``` + +**Angular Adapter (Class-Based, Less Reactive):** +Angular prefers explicit Input bindings and lifecycle hooks or `AsyncPipe`. The Angular adapter takes the decoder stream and manages `ChangeDetectorRef`. It hooks into the `ngOnDestroy` lifecycle event to guarantee disposal. + +```typescript +// angular_adapter.ts +export function createAngularComponent( + frontEnd: FrontEndLayer, + ComponentClass: Type // The Angular Component Class +): AngularComponentRenderer { + return { + name: frontEnd.name, + schema: frontEnd.schema, + render: (ctx: ComponentContext, viewContainerRef: ViewContainerRef) => { + // 1. Instantiates the Angular Component via ViewContainerRef. + // 2. Creates the decoder: `const decoder = frontEnd.createDecoder(ctx);` + // 3. Subscribes to `decoder.propsStream` and explicitly updates component instance inputs. + // 4. Calls `changeDetectorRef.detectChanges()`. + // 5. Explicitly registers `decoder.dispose()` to fire when the ViewContainerRef is destroyed + // (or via an ngOnDestroy hook on the wrapper), cleaning up the DataModel bindings. + return new AngularAdapterWrapper(ctx, frontEnd, ComponentClass, viewContainerRef); + } + }; +} + +// Usage in an app: +@Component({ + selector: 'app-button', + template: `` +}) +export class AngularButtonComponent { + @Input() label: string = ''; + @Input() action: () => void = () => {}; +} + +const NgButton = createAngularComponent(ButtonFrontEnd, AngularButtonComponent); +``` + +### 4. Strongly-Typed Catalog Implementations +To solve the problem of ensuring all components are properly implemented *and* match the exact API signature, we use TypeScript intersection types. This ensures that a provided renderer not only exists, but its `name` and `schema` strictly match the official Catalog Definition. + +```typescript +// basic_catalog_api/implementation.ts + +// Define the base constraint for any framework renderer +export interface BaseRenderer { + name: string; + schema: z.ZodTypeAny; +} + +// The implementation map forces the framework renderer to intersect with the exact definition +export type BasicCatalogImplementation = { + Button: TRenderer & { name: "Button", schema: typeof ButtonDef.schema }; + Text: TRenderer & { name: "Text", schema: typeof TextDef.schema }; + Row: TRenderer & { name: "Row", schema: typeof RowDef.schema }; + Column: TRenderer & { name: "Column", schema: typeof ColumnDef.schema }; + // ... all basic components +}; + +// Angular implementation Example +interface AngularComponentRenderer extends BaseRenderer { + // Angular-specific render method + render: (ctx: ComponentContext, vcr: ViewContainerRef) => any; +} + +export function createAngularBasicCatalog( + implementations: BasicCatalogImplementation +): Catalog { + return new Catalog( + "https://a2ui.org/basic_catalog.json", + Object.values(implementations) + ); +} + +// Usage +const basicCatalog = createAngularBasicCatalog({ + // If NgButton's `name` is not exactly "Button", or if its + // `schema` doesn't match ButtonDef.schema exactly, TypeScript throws an error! + Button: NgButton, + Text: NgText, + Row: NgRow, + Column: NgColumn, + // ... +}); +``` + +--- + +## Streamlined DX: The `defineCatalog` Approach + +To provide a super streamlined, `json-render`-style API for TypeScript users, we can build abstraction helpers on top of the Decoder architecture. This avoids the boilerplate of defining schemas, decoders, and implementations across multiple files. + +### Conceptual Streamlined API (TypeScript Web Core) + +Using the generic `ZodDecoder` capability, we can construct the API directly mapping to standard A2UI types (`ChildListSchema`, `ComponentIdSchema`). + +**1. Defining the Catalog API & Schemas:** +```typescript +export const myCatalogDef = defineCatalogApi({ + id: "my-custom-catalog", + components: { + Card: { + props: z.object({ + title: DynamicStringSchema, + description: DynamicStringSchema.optional(), + child: ComponentIdSchema // Explicit A2UI component relationship + }) + }, + Button: { + props: z.object({ + label: DynamicStringSchema, + action: ActionSchema + }) + } + } +}); +``` + +**2. Implementing the React Catalog:** +The `defineCatalogImplementation` function automatically generates the generic `Decoder` under the hood based on the Zod schema provided in step 1, resolving dynamic values into static values passed directly to the render function. + +For structural links like `ChildList` or `ComponentId`, the framework adapter automatically intercepts these properties and provides framework-native rendering helpers (like `renderChild(props.child)`). + +```typescript +export const myReactCatalog = defineCatalogImplementation(myCatalogDef, { + components: { + // `props` here are fully resolved strings and callbacks! + // `renderChild` is injected by the adapter to render A2UI children. + Card: ({ props, renderChild }) => ( +
+

{props.title}

+ {props.description &&

{props.description}

} + {renderChild(props.child)} +
+ ), + + Button: ({ props }) => ( + + ) + }, + functions: { + submit_form: async (params, context) => { ... } + } +}); +``` + +## Summary of Changes to the Renderer Guide + +To implement these updates in `renderer_guide.md`, we will: + +1. **Introduce the Decoder Layer Concept**: Detail that frameworks should not manually subscribe to `DataModel` paths. Instead, they should utilize a shared `ComponentDecoder` layer that outputs a framework-agnostic reactive stream of resolved properties. +2. **Define Framework Adapters**: Add guidance on creating framework-specific adapters (e.g., React and Angular) that handle the lifecycle of the Decoder (subscription and disposal) and map its stream to native framework reactivity paradigms. +3. **Strict Catalog Typing Strategy**: Recommend that Catalog implementations expose a generic `CatalogImplementation` mapping interface so compiler errors enforce that implementations strictly match the required schema and naming signatures. +4. **Mention Advanced DX Utilities**: Outline how TypeScript implementations can leverage schema-reflection via Zod to auto-generate Decoders (`createZodDecoder`), while static languages can rely on code-generation for streamlined development. \ No newline at end of file From 6ae4b2432ed6b3f24849a4704c04b659316f4c2b Mon Sep 17 00:00:00 2001 From: Jacob Simionato Date: Fri, 6 Mar 2026 12:53:08 +1030 Subject: [PATCH 05/13] Update catalog proposal --- .../v0_9/docs/catalog_api_proposal.md | 185 ++++++++++++------ 1 file changed, 126 insertions(+), 59 deletions(-) diff --git a/specification/v0_9/docs/catalog_api_proposal.md b/specification/v0_9/docs/catalog_api_proposal.md index 97f456a56..0bf467dfb 100644 --- a/specification/v0_9/docs/catalog_api_proposal.md +++ b/specification/v0_9/docs/catalog_api_proposal.md @@ -3,10 +3,17 @@ ## Objective To refine the A2UI client-side Catalog API to clearly separate component schema definitions, data decoding logic, and framework-specific rendering. This will improve code sharing across frameworks, enhance type safety when implementing catalogs, and provide a streamlined, idiomatic developer experience for creating custom components. +## Use Cases +This architecture is designed to support the following scenarios when working with Catalogs: +- Implement a Component from scratch with a new API and new one-off implementation +- Implement a Component based on an API which is defined elsewhere +- Implement a Component using an API and a binder layer already implemented, and potentially shared across different UI frameworks for the same language +- Implement an entire Catalog to match a predefined Catalog API, with type safety to ensure I include all the correct components and schemas + ## Requirements Addressed 1. **Share Component Schemas**: Allow developers to declare component schemas independently so they can be reused across different platform implementations. -2. **Share Decoding Logic**: Centralize the boilerplate of resolving `DynamicValue` properties, handling reactive streams, and tracking subscriptions into a framework-agnostic "Decoder" layer. Provide details on how a generic decoder can be built using Zod for web environments. +2. **Share Decoding Logic**: Centralize the boilerplate of resolving `DynamicValue` properties, handling reactive streams, and tracking subscriptions into a framework-agnostic "Binder" layer. Provide details on how a generic binder can be built using Zod for web environments. 3. **Reliable Catalog Implementation**: Provide a strongly-typed mechanism to ensure a specific framework implementation covers all required components of a defined catalog (e.g., ensuring `createAngularBasicCatalog` includes a renderer for `Button`, `Text`, etc.). 4. **Framework-Specific Adapters**: Provide idiomatic APIs for specific frameworks (e.g., a React adapter and an Angular adapter that provide standard framework props/inputs instead of raw `ComponentContext`). 5. **Streamlined DX (Stretch Goal)**: Explore a `defineCatalogImplementation` API for defining catalogs quickly with high type safety. @@ -18,7 +25,10 @@ To refine the A2UI client-side Catalog API to clearly separate component schema We propose breaking down the component lifecycle into four distinct layers: ### 1. Component Schema (API Definition) -This layer defines the exact JSON footprint of a component without any rendering or decoding logic. It is the single source of truth for the component's contract, exactly mirroring the A2UI component schema definitions. +This layer defines the exact JSON footprint of a component without any rendering or decoding logic. It acts as the single source of truth for the component's contract, exactly mirroring the A2UI component schema definitions. The goal is to define the properties a component accepts (like `label` or `action`) using the platform's preferred schema validation or serialization library. + +#### TypeScript/Web Example +In a web environment, this is typically done using Zod to represent the JSON Schema. ```typescript // basic_catalog_api/schemas.ts @@ -39,114 +49,162 @@ export const ButtonDef: ComponentDefinition<"Button", typeof ButtonSchema> = { }; ``` -### 2. The Decoder Layer (Framework-Agnostic) -A2UI components are heavily reliant on `DynamicValue` bindings (which resolve to reactive streams). Framework renderers currently have to manually resolve these, manage context, and handle subscriptions. The **Decoder Layer** absorbs this responsibility. +*Note for Swift/Kotlin:* In Swift, this would be represented by `Codable` structs mapping to the JSON structure. In Kotlin, this would be `kotlinx.serialization` classes. The architectural separation of concerns remains identical. + +### 2. The Binder Layer (Framework-Agnostic) +A2UI components are heavily reliant on `DynamicValue` bindings, which must be resolved into reactive streams. Framework renderers currently have to manually resolve these, manage context, and handle the lifecycle of data subscriptions. + +The **Binder Layer** absorbs this responsibility. It takes the raw component properties and the `ComponentContext`, and transforms the reactive A2UI bindings into a single, cohesive stream of strongly-typed `ResolvedProps`. + +#### Subscription Lifecycle and Cleanup +A critical responsibility of the Binding is tracking all subscriptions it creates against the underlying data model. When a component is removed from the UI (because its parent was replaced, the surface was deleted, etc.), the underlying framework must call the Binding's `dispose()` method. The Binding then iterates through its internally tracked subscription list and severs them, ensuring that no dangling listeners remain attached to the global `DataModel`. -A Decoder takes the raw `ComponentContext` and transforms the reactive A2UI bindings into a single, cohesive stream of strongly-typed `ResolvedProps`. +#### Generic Interface Concept +Conceptually, the binder layer looks like this in any language: ```typescript -// The generic Decoder interface -export interface ComponentDecoder { - // A stream of fully resolved, ready-to-render props - readonly propsStream: Observable; +// The generic Binding interface representing an active connection +export interface ComponentBinding { + // A stateful stream of fully resolved, ready-to-render props. + // It must hold the current value so frameworks can read the initial state synchronously. + readonly propsStream: BehaviorSubject; // Or StateFlow/CurrentValueSubject + // Cleans up all underlying data model subscriptions dispose(): void; } -// The Middle Layer combining Schema + Decoder Logic -export interface FrontEndLayer { +// The Binder definition combining Schema + Binding Logic +export interface ComponentBinder { name: string; - schema: z.ZodType; - createDecoder(context: ComponentContext): ComponentDecoder; + schema: any; // Platform specific schema type + bind(context: ComponentContext): ComponentBinding; } ``` -#### Subscription Lifecycle and Cleanup -A critical responsibility of the Decoder is tracking all subscriptions it creates (via `context.dataContext.subscribeDynamicValue`). +*Note on Stateful Streams:* The `propsStream` MUST be a stateful stream (e.g., `BehaviorSubject` in RxJS, `StateFlow` in Kotlin Coroutines, or `CurrentValueSubject` in Swift Combine). UI frameworks typically require an initial state to render the first frame synchronously. Because `DataContext.subscribeDynamicValue()` resolves its initial value synchronously, the binder can immediately seed the `BehaviorSubject` with the fully resolved initial properties. -When a component is removed from the UI (because its parent was replaced, the surface was deleted, etc.), the underlying framework must call the Decoder's `dispose()` method. The Decoder then iterates through its internally tracked subscription list and calls `unsubscribe()` on each one, ensuring that no dangling listeners remain attached to the global `DataModel`. +#### Generic Binders via Zod (Web Implementation Example) +For TypeScript/Web implementations, one approach is to write a generic `ZodBinder` that automatically infers subscriptions. Instead of writing custom logic for every component, the binder recursively inspects the Zod schema. -#### Generic Decoders via Zod (Web Implementation) -For TypeScript/Web implementations, we can write a generic `ZodDecoder` that automatically infers subscriptions. Instead of writing custom logic for every component, the decoder recursively inspects the Zod schema. - -When the `ZodDecoder` walks the schema and encounters known A2UI dynamic types (e.g., `DynamicStringSchema`), it automatically invokes `context.dataContext.subscribeDynamicValue()`. It stores the returned subscription objects in an internal array. When `dispose()` is called, it loops through this array and unsubscribes them all. +When the `ZodBinder` walks the schema and encounters known A2UI dynamic types (e.g., `DynamicStringSchema`), it automatically invokes `context.dataContext.subscribeDynamicValue()`. It stores the returned subscription objects in an internal array. When `dispose()` is called, it loops through this array and unsubscribes them all. ```typescript -// Conceptual Generic Zod Decoder Factory -export function createZodDecoder( +// Illustrative Generic Zod Binding Factory +export function createZodBinding( schema: T, context: ComponentContext -): ComponentDecoder> { +): ComponentBinding> { // 1. Walk the schema to find all DynamicValue and Action properties. // 2. Map `DynamicValue` properties to `context.dataContext.subscribeDynamicValue()` // and store the returned `DataSubscription` objects. // 3. Map `Action` properties to `context.dispatchAction()`. - // 4. Combine all observables (e.g., using `combineLatest` in RxJS) into a single stream. - // 5. Return an object conforming to ComponentDecoder whose `dispose()` method unsubscribes all stored subscriptions. + // 4. Combine all observables (e.g., using `combineLatest` in RxJS) into a single stateful stream. + // 5. Return an object conforming to ComponentBinding whose `dispose()` method unsubscribes all stored subscriptions. - return new GenericZodDecoder(schema, context); + return new GenericZodBinding(schema, context); } -// Button implementation becomes trivial: -export const ButtonFrontEnd: FrontEndLayer = { +// Button implementation becomes simplified: +export const ButtonBinder: ComponentBinder = { name: "Button", schema: ButtonDef.schema, - createDecoder: (ctx) => createZodDecoder(ButtonDef.schema, ctx) + bind: (ctx) => createZodBinding(ButtonDef.schema, ctx) }; ``` -*Note for Static Languages (Swift/Kotlin):* Dynamic runtime reflection isn't as easily feasible. Swift/Kotlin environments can rely on Code Generation (Swift Macros, KSP) to generate the boilerplate `Decoder` logic based on the schema at compile-time. +*Note for Static Languages (Swift/Kotlin):* Dynamic runtime reflection isn't as easily feasible. Swift/Kotlin environments can rely on Code Generation (Swift Macros, KSP) to generate the boilerplate `Binding` logic based on the schema at compile-time. + +#### Alternative: Binderless Implementation (Direct Binding) +For frameworks that are less dynamic, lack codegen systems, or for developers who simply want to implement a single, one-off component without the abstraction overhead of a generic binder, it is perfectly valid to skip the formal binder layer and implement the component directly. + +In a "binderless" setup, the developer creates the component in one step. The system directly receives the schema, and the render function takes the raw `ComponentContext` (or a lightweight framework-specific wrapper around it), manually subscribing to dynamic properties and returning the native UI element. + +**Dart/Flutter Illustrative Example:** +```dart +// direct_component.dart + +// The developer defines the component in one unified step without a separate binder. +final myButtonComponent = FrameworkComponent( + name: 'Button', + schema: buttonSchema, // A schematic representation of the properties + + // The render function handles reading from context and building the widget. + // It receives the A2UI ComponentContext and a helper to build children. + render: (ComponentContext context, Widget Function(String) buildChild) { + // 1. Manually resolve or subscribe to dynamic values. + // (In Flutter, this might be wrapped in a StreamBuilder or custom hook + // that handles the unsubscription automatically on dispose). + return StreamBuilder( + stream: context.dataContext.observeDynamicValue(context.componentModel.properties['label']), + builder: (context, snapshot) { + return ElevatedButton( + onPressed: () { + context.dispatchAction(context.componentModel.properties['action']); + }, + child: Text(snapshot.data?.toString() ?? ''), + ); + } + ); + } +); +``` +While this approach bypasses the reusable binder layer, it offers a straightforward path for adding custom components and remains fully compliant with the architecture's boundaries. ### 3. Framework-Specific Adapters -Framework developers should not interact with `ComponentContext` or `Decoder` directly when writing the actual UI view. We provide framework-specific adapters that bridge the `Decoder`'s stream to the framework's native reactivity and automatically handle the disposal lifecycle to guarantee memory safety. +Framework developers should not interact with `ComponentContext` or `ComponentBinding` directly when writing the actual UI view. Instead, the architecture should provide framework-specific adapters that bridge the `Binding`'s stream to the framework's native reactivity and automatically handle the disposal lifecycle to guarantee memory safety. + +The adapter acts as a wrapper that: +1. Instantiates the binder (obtaining a `ComponentBinding`). +2. Binds the binding's output stream to the framework's state mechanism. +3. Passes the resolved values directly into the developer's view implementation. +4. Hooks into the framework's native destruction lifecycle to call `binding.dispose()`. -**React Adapter (Highly Reactive):** -React supports subscribing to external stores directly. The adapter leverages `useSyncExternalStore` or `useEffect` to hook into the decoder's stream, and uses the `useEffect` cleanup return function to dispose of the decoder when the component unmounts. +#### React Adapter Illustrative Example +React supports subscribing to external stores directly. An adapter might leverage utilities like `useSyncExternalStore` or `useEffect` to hook into the binding's stream, using the native cleanup mechanisms to dispose of the binding when the component unmounts. ```typescript // react_adapter.ts export function createReactComponent( - frontEnd: FrontEndLayer, + binder: ComponentBinder, RenderComponent: React.FC ): ReactComponentRenderer { return { - name: frontEnd.name, - schema: frontEnd.schema, + name: binder.name, + schema: binder.schema, render: (ctx: ComponentContext) => { // Adapter maps `propsStream` into React state. - // Crucially, it registers `decoder.dispose()` inside a `useEffect` cleanup block + // One common pattern is registering `binding.dispose()` inside a `useEffect` cleanup block // so when React unmounts this component, the DataModel subscriptions are severed. - return ; + return ; } }; } // Usage: -const ReactButton = createReactComponent(ButtonFrontEnd, (props) => ( +const ReactButton = createReactComponent(ButtonBinder, (props) => ( // `label` is purely a string here )); ``` -**Angular Adapter (Class-Based, Less Reactive):** -Angular prefers explicit Input bindings and lifecycle hooks or `AsyncPipe`. The Angular adapter takes the decoder stream and manages `ChangeDetectorRef`. It hooks into the `ngOnDestroy` lifecycle event to guarantee disposal. +#### Angular Adapter Illustrative Example +Angular often utilizes explicit Input bindings and lifecycle hooks. An Angular adapter might take the binding stream and manage updates via `ChangeDetectorRef` or the `AsyncPipe`. ```typescript // angular_adapter.ts export function createAngularComponent( - frontEnd: FrontEndLayer, + binder: ComponentBinder, ComponentClass: Type // The Angular Component Class ): AngularComponentRenderer { return { - name: frontEnd.name, - schema: frontEnd.schema, + name: binder.name, + schema: binder.schema, render: (ctx: ComponentContext, viewContainerRef: ViewContainerRef) => { - // 1. Instantiates the Angular Component via ViewContainerRef. - // 2. Creates the decoder: `const decoder = frontEnd.createDecoder(ctx);` - // 3. Subscribes to `decoder.propsStream` and explicitly updates component instance inputs. - // 4. Calls `changeDetectorRef.detectChanges()`. - // 5. Explicitly registers `decoder.dispose()` to fire when the ViewContainerRef is destroyed - // (or via an ngOnDestroy hook on the wrapper), cleaning up the DataModel bindings. - return new AngularAdapterWrapper(ctx, frontEnd, ComponentClass, viewContainerRef); + // 1. Instantiates the Angular Component. + // 2. Creates the binding via binder.bind(ctx). + // 3. Subscribes to `binding.propsStream` and updates component instance inputs. + // 4. Manages change detection. + // 5. Hooks into native destruction (e.g. ngOnDestroy) to call `binding.dispose()`. + return new AngularAdapterWrapper(ctx, binder, ComponentClass, viewContainerRef); } }; } @@ -161,11 +219,20 @@ export class AngularButtonComponent { @Input() action: () => void = () => {}; } -const NgButton = createAngularComponent(ButtonFrontEnd, AngularButtonComponent); +const NgButton = createAngularComponent(ButtonBinder, AngularButtonComponent); ``` +#### SwiftUI / Compose Illustrative Concepts +* **SwiftUI:** An adapter might wrap the binding's publisher into an `@ObservedObject` or `@StateObject`. The `dispose()` call could be placed in the `.onDisappear` modifier or within the `deinit` block of the observable object. +* **Jetpack Compose:** An adapter might convert a `StateFlow` to Compose state using utilities like `collectAsState()`. The `dispose()` call could be managed using a `DisposableEffect` keyed on the component instance. + ### 4. Strongly-Typed Catalog Implementations -To solve the problem of ensuring all components are properly implemented *and* match the exact API signature, we use TypeScript intersection types. This ensures that a provided renderer not only exists, but its `name` and `schema` strictly match the official Catalog Definition. +To solve the problem of ensuring all components are properly implemented *and* match the exact API signature, platforms with strong type systems should utilize their advanced typing features (like intersection types in TypeScript or protocols/interfaces in Swift/Kotlin). + +This ensures that a provided renderer not only exists, but its `name` and `schema` strictly match the official Catalog Definition, catching mismatches at compile time rather than runtime. + +#### TypeScript Implementation Example +We use TypeScript intersection types to force the framework renderer to intersect with the exact definition. ```typescript // basic_catalog_api/implementation.ts @@ -216,11 +283,11 @@ const basicCatalog = createAngularBasicCatalog({ ## Streamlined DX: The `defineCatalog` Approach -To provide a super streamlined, `json-render`-style API for TypeScript users, we can build abstraction helpers on top of the Decoder architecture. This avoids the boilerplate of defining schemas, decoders, and implementations across multiple files. +To provide a super streamlined, `json-render`-style API for TypeScript users, we can build abstraction helpers on top of the Binder architecture. This avoids the boilerplate of defining schemas, binders, and implementations across multiple files. ### Conceptual Streamlined API (TypeScript Web Core) -Using the generic `ZodDecoder` capability, we can construct the API directly mapping to standard A2UI types (`ChildListSchema`, `ComponentIdSchema`). +Using the generic `ZodBinder` capability, we can construct the API directly mapping to standard A2UI types (`ChildListSchema`, `ComponentIdSchema`). **1. Defining the Catalog API & Schemas:** ```typescript @@ -245,7 +312,7 @@ export const myCatalogDef = defineCatalogApi({ ``` **2. Implementing the React Catalog:** -The `defineCatalogImplementation` function automatically generates the generic `Decoder` under the hood based on the Zod schema provided in step 1, resolving dynamic values into static values passed directly to the render function. +The `defineCatalogImplementation` function automatically generates the generic `Binding` under the hood based on the Zod schema provided in step 1, resolving dynamic values into static values passed directly to the render function. For structural links like `ChildList` or `ComponentId`, the framework adapter automatically intercepts these properties and provides framework-native rendering helpers (like `renderChild(props.child)`). @@ -278,7 +345,7 @@ export const myReactCatalog = defineCatalogImplementation(myCatalogDef, { To implement these updates in `renderer_guide.md`, we will: -1. **Introduce the Decoder Layer Concept**: Detail that frameworks should not manually subscribe to `DataModel` paths. Instead, they should utilize a shared `ComponentDecoder` layer that outputs a framework-agnostic reactive stream of resolved properties. -2. **Define Framework Adapters**: Add guidance on creating framework-specific adapters (e.g., React and Angular) that handle the lifecycle of the Decoder (subscription and disposal) and map its stream to native framework reactivity paradigms. +1. **Introduce the Binder Layer Concept**: Detail that frameworks should not manually subscribe to `DataModel` paths. Instead, they should utilize a shared `ComponentBinder` layer that outputs a framework-agnostic reactive stream of resolved properties (`ComponentBinding`). +2. **Define Framework Adapters**: Add guidance on creating framework-specific adapters (e.g., React and Angular) that handle the lifecycle of the Binding (subscription and disposal) and map its stream to native framework reactivity paradigms. 3. **Strict Catalog Typing Strategy**: Recommend that Catalog implementations expose a generic `CatalogImplementation` mapping interface so compiler errors enforce that implementations strictly match the required schema and naming signatures. -4. **Mention Advanced DX Utilities**: Outline how TypeScript implementations can leverage schema-reflection via Zod to auto-generate Decoders (`createZodDecoder`), while static languages can rely on code-generation for streamlined development. \ No newline at end of file +4. **Mention Advanced DX Utilities**: Outline how TypeScript implementations can leverage schema-reflection via Zod to auto-generate Binders (`createZodBinding`), while static languages can rely on code-generation for streamlined development. \ No newline at end of file From 0e75794377e5fe5b6564ba66a68a8e7c4b6a6e2a Mon Sep 17 00:00:00 2001 From: Jacob Simionato Date: Fri, 6 Mar 2026 13:33:37 +1030 Subject: [PATCH 06/13] Improve catalog API proposal --- .../v0_9/docs/catalog_api_proposal.md | 109 +++++++++++++----- 1 file changed, 79 insertions(+), 30 deletions(-) diff --git a/specification/v0_9/docs/catalog_api_proposal.md b/specification/v0_9/docs/catalog_api_proposal.md index 0bf467dfb..2e262dc03 100644 --- a/specification/v0_9/docs/catalog_api_proposal.md +++ b/specification/v0_9/docs/catalog_api_proposal.md @@ -5,6 +5,7 @@ To refine the A2UI client-side Catalog API to clearly separate component schema ## Use Cases This architecture is designed to support the following scenarios when working with Catalogs: +- Declare a Component or Catalog with a fixed API which could have multiple implementations. - Implement a Component from scratch with a new API and new one-off implementation - Implement a Component based on an API which is defined elsewhere - Implement a Component using an API and a binder layer already implemented, and potentially shared across different UI frameworks for the same language @@ -18,6 +19,26 @@ This architecture is designed to support the following scenarios when working wi 4. **Framework-Specific Adapters**: Provide idiomatic APIs for specific frameworks (e.g., a React adapter and an Angular adapter that provide standard framework props/inputs instead of raw `ComponentContext`). 5. **Streamlined DX (Stretch Goal)**: Explore a `defineCatalogImplementation` API for defining catalogs quickly with high type safety. +## Implementation Topologies +Because A2UI spans multiple languages and UI paradigms, the strictness and location of these architectural boundaries will vary depending on the target ecosystem. + +### Dynamic Languages (e.g., TypeScript / JavaScript) +In highly dynamic ecosystems like the web, the architecture is typically split across multiple packages to maximize code reuse across diverse UI frameworks (React, Angular, Vue, Lit). +* **Core Library (`web_core`)**: Implements Layer 1 (Component Schemas) and Layer 2 (The Binder Layer). Because TS/JS has powerful runtime reflection, the core library provides a *Generic Zod Binder* that automatically handles all data binding without framework-specific code. +* **Framework Library (`react_renderer`, `angular_renderer`)**: Implements Layer 3 (Framework-Specific Adapters) and Layer 4 (Strongly-Typed Catalog Implementations). It provides the adapter utilities (`createReactComponent`) and the actual view implementations (the React `Button`, `Text`, etc.). + +### Static Languages (e.g., Kotlin, Swift) +In statically typed languages, runtime reflection is often limited or discouraged for performance reasons. +* **Core Library (e.g., `kotlin_core`)**: Implements Layer 1 (Component Schemas). For Layer 2, the core library typically provides a manually implemented **Binder Layer** for the standard Basic Catalog components. This ensures that even in static environments, basic components have a standardized, framework-agnostic reactive state definition. +* **Code Generation (Future/Optional)**: While the core library starts with manual binders, it may eventually offer **Code Generation** (e.g., KSP, Swift Macros) to automate the creation of Binders for custom components. +* **Custom Components**: In the absence of code generation, developers implementing new, ad-hoc components typically utilize the **"Binderless" Implementation** flow (see Layer 2 Alternative), which allows for direct binding to the data model without intermediate boilerplate. +* **Framework Library (e.g., `compose_renderer`)**: Implements Layer 3 (Adapters) and Layer 4. Uses the predefined Binders to connect to native UI state. + +### Combined Core + Framework Libraries (e.g., Swift + SwiftUI) +In ecosystems dominated by a single UI framework (like iOS with SwiftUI), developers often build a single, unified library rather than splitting Core and Framework into separate packages. +* **Relaxed Boundaries**: The strict separation between Core and Framework libraries can be relaxed. The generic `ComponentContext` and the framework-specific adapter logic are often tightly integrated. +* **Why Keep the Binder Layer?**: Even in a combined library, defining the intermediate **Binder Layer (Layer 2)** remains highly recommended. It standardizes how A2UI data resolves into reactive state (e.g., standardizing the `ComponentBinding` interface). This allows developers adopting the library to easily write alternative implementations of well-known components (e.g., swapping the default SwiftUI Button with a custom corporate-branded SwiftUI Button) without having to rewrite the complex, boilerplate-heavy A2UI data subscription logic. + --- ## Proposed Architecture: The 4-Layer Model @@ -32,8 +53,8 @@ In a web environment, this is typically done using Zod to represent the JSON Sch ```typescript // basic_catalog_api/schemas.ts -export interface ComponentDefinition { - name: Name; +export interface ComponentDefinition { + name: string; schema: PropsSchema; } @@ -43,10 +64,10 @@ const ButtonSchema = z.object({ }); // Example definition -export const ButtonDef: ComponentDefinition<"Button", typeof ButtonSchema> = { - name: "Button", +export const ButtonDef = { + name: "Button" as const, schema: ButtonSchema -}; +} satisfies ComponentDefinition; ``` *Note for Swift/Kotlin:* In Swift, this would be represented by `Codable` structs mapping to the JSON structure. In Kotlin, this would be `kotlinx.serialization` classes. The architectural separation of concerns remains identical. @@ -57,7 +78,7 @@ A2UI components are heavily reliant on `DynamicValue` bindings, which must be re The **Binder Layer** absorbs this responsibility. It takes the raw component properties and the `ComponentContext`, and transforms the reactive A2UI bindings into a single, cohesive stream of strongly-typed `ResolvedProps`. #### Subscription Lifecycle and Cleanup -A critical responsibility of the Binding is tracking all subscriptions it creates against the underlying data model. When a component is removed from the UI (because its parent was replaced, the surface was deleted, etc.), the underlying framework must call the Binding's `dispose()` method. The Binding then iterates through its internally tracked subscription list and severs them, ensuring that no dangling listeners remain attached to the global `DataModel`. +A critical responsibility of the Binding is tracking all subscriptions it creates against the underlying data model. As outlined in the Contract of Ownership (see Framework-Specific Adapters), the framework adapter manages the lifecycle of the Binding. When a component is removed from the UI (because its parent was replaced, the surface was deleted, etc.), the framework adapter must call the Binding's `dispose()` method. The Binding then iterates through its internally tracked subscription list and severs them, ensuring that no dangling listeners remain attached to the global `DataModel`. #### Generic Interface Concept Conceptually, the binder layer looks like this in any language: @@ -67,21 +88,20 @@ Conceptually, the binder layer looks like this in any language: export interface ComponentBinding { // A stateful stream of fully resolved, ready-to-render props. // It must hold the current value so frameworks can read the initial state synchronously. - readonly propsStream: BehaviorSubject; // Or StateFlow/CurrentValueSubject + readonly propsStream: StatefulStream; // e.g. BehaviorSubject, StateFlow, or CurrentValueSubject // Cleans up all underlying data model subscriptions dispose(): void; } // The Binder definition combining Schema + Binding Logic -export interface ComponentBinder { - name: string; - schema: any; // Platform specific schema type +// By extending ComponentDefinition, a Binder can be used anywhere a pure schema definition is expected. +export interface ComponentBinder extends ComponentDefinition { bind(context: ComponentContext): ComponentBinding; } ``` -*Note on Stateful Streams:* The `propsStream` MUST be a stateful stream (e.g., `BehaviorSubject` in RxJS, `StateFlow` in Kotlin Coroutines, or `CurrentValueSubject` in Swift Combine). UI frameworks typically require an initial state to render the first frame synchronously. Because `DataContext.subscribeDynamicValue()` resolves its initial value synchronously, the binder can immediately seed the `BehaviorSubject` with the fully resolved initial properties. +*Note on Stateful Streams:* The `propsStream` should ideally be a stateful stream (common examples include `BehaviorSubject` in RxJS, `StateFlow` in Kotlin Coroutines, or `CurrentValueSubject` in Swift Combine). UI frameworks typically require an initial state to render the first frame synchronously. Because `DataContext.subscribeDynamicValue()` resolves its initial value synchronously, the binder can immediately seed the stream with the fully resolved initial properties. #### Generic Binders via Zod (Web Implementation Example) For TypeScript/Web implementations, one approach is to write a generic `ZodBinder` that automatically infers subscriptions. Instead of writing custom logic for every component, the binder recursively inspects the Zod schema. @@ -104,10 +124,9 @@ export function createZodBinding( return new GenericZodBinding(schema, context); } -// Button implementation becomes simplified: -export const ButtonBinder: ComponentBinder = { - name: "Button", - schema: ButtonDef.schema, +// Button implementation becomes simplified by leveraging the existing ButtonDef: +export const ButtonBinder: ComponentBinder = { + ...ButtonDef, bind: (ctx) => createZodBinding(ButtonDef.schema, ctx) }; ``` @@ -153,20 +172,36 @@ While this approach bypasses the reusable binder layer, it offers a straightforw ### 3. Framework-Specific Adapters Framework developers should not interact with `ComponentContext` or `ComponentBinding` directly when writing the actual UI view. Instead, the architecture should provide framework-specific adapters that bridge the `Binding`'s stream to the framework's native reactivity and automatically handle the disposal lifecycle to guarantee memory safety. +#### Contract of Ownership +A crucial part of A2UI's architecture is understanding who "owns" the data layers. +* **The Data Layer (Message Processor) owns the `ComponentModel`**. It creates, updates, and destroys the component's raw data state based on the incoming JSON stream. +* **The Framework Adapter owns the `ComponentContext` and `ComponentBinding`**. When the native framework decides to mount a component onto the screen (e.g., React runs `render`, Flutter runs `build`), the Framework Adapter creates the `ComponentContext` and passes it to the Binder to create a `ComponentBinding`. When the native framework unmounts the component, the Framework Adapter MUST call `binding.dispose()`. + +#### Data Props vs. Structural Props +It's important to distinguish between Data Props (like `label` or `value`) and Structural Props (like `child` or `children`). +* **Data Props:** Handled entirely by the Binder. The adapter receives a stream of fully resolved values (e.g., `"Submit"` instead of a `DynamicString`). +* **Structural Props:** The Binder does not attempt to resolve component IDs into actual UI trees. Instead, it outputs metadata for the children that need to be rendered. + * For a simple `ComponentId` (e.g., `Card.child`), it emits an object like `{ id: string, basePath: string }`. + * For a `ChildList` (e.g., `Column.children`), it evaluates the array and emits a `ChildNode[]` stream. If the `ChildList` is a template, the Binder subscribes to the array in the `DataModel` and maps each item to `{ id: templateId, basePath: '/path/to/item/index' }`. +* The framework adapter is then responsible for taking these node definitions and calling a framework-native `buildChild(id, basePath)` method. + The adapter acts as a wrapper that: 1. Instantiates the binder (obtaining a `ComponentBinding`). 2. Binds the binding's output stream to the framework's state mechanism. -3. Passes the resolved values directly into the developer's view implementation. -4. Hooks into the framework's native destruction lifecycle to call `binding.dispose()`. +3. Injects structural rendering helpers (like `buildChild`) alongside the resolved data properties. +4. Passes everything into the developer's view implementation. +5. Hooks into the framework's native destruction lifecycle to call `binding.dispose()`. #### React Adapter Illustrative Example -React supports subscribing to external stores directly. An adapter might leverage utilities like `useSyncExternalStore` or `useEffect` to hook into the binding's stream, using the native cleanup mechanisms to dispose of the binding when the component unmounts. +React supports subscribing to external stores directly. An adapter might leverage utilities like `useSyncExternalStore` or `useEffect` to hook into the binding's stream, using the native cleanup mechanisms to dispose of the binding when the component unmounts. It also provides a `buildChild` helper. ```typescript // react_adapter.ts +export interface ChildNode { id: string; basePath?: string; } + export function createReactComponent( binder: ComponentBinder, - RenderComponent: React.FC + RenderComponent: React.FC<{ props: Resolved, buildChild: (node: ChildNode) => React.ReactNode }> ): ReactComponentRenderer { return { name: binder.name, @@ -175,14 +210,33 @@ export function createReactComponent( // Adapter maps `propsStream` into React state. // One common pattern is registering `binding.dispose()` inside a `useEffect` cleanup block // so when React unmounts this component, the DataModel subscriptions are severed. + // The wrapper also provides the `buildChild` implementation. return ; } }; } -// Usage: -const ReactButton = createReactComponent(ButtonBinder, (props) => ( - // `label` is purely a string here +// Usage (Button - Data Props only): +const ReactButton = createReactComponent(ButtonBinder, ({ props }) => ( + +)); + +// Usage (Card - Structural Props): +const ReactCard = createReactComponent(CardBinder, ({ props, buildChild }) => ( +
+ {buildChild(props.child)} +
+)); + +// Usage (Column - ChildList Props): +const ReactColumn = createReactComponent(ColumnBinder, ({ props, buildChild }) => ( +
+ {props.children.map((childNode, index) => ( + + {buildChild(childNode)} + + ))} +
)); ``` @@ -237,14 +291,8 @@ We use TypeScript intersection types to force the framework renderer to intersec ```typescript // basic_catalog_api/implementation.ts -// Define the base constraint for any framework renderer -export interface BaseRenderer { - name: string; - schema: z.ZodTypeAny; -} - // The implementation map forces the framework renderer to intersect with the exact definition -export type BasicCatalogImplementation = { +export type BasicCatalogImplementation> = { Button: TRenderer & { name: "Button", schema: typeof ButtonDef.schema }; Text: TRenderer & { name: "Text", schema: typeof TextDef.schema }; Row: TRenderer & { name: "Row", schema: typeof RowDef.schema }; @@ -253,7 +301,8 @@ export type BasicCatalogImplementation = { }; // Angular implementation Example -interface AngularComponentRenderer extends BaseRenderer { +// By extending ComponentDefinition, we ensure the renderer carries the required API metadata +interface AngularComponentRenderer extends ComponentDefinition { // Angular-specific render method render: (ctx: ComponentContext, vcr: ViewContainerRef) => any; } From 2f52cad9462c8a1a6a60699eaf91588921a65e77 Mon Sep 17 00:00:00 2001 From: Jacob Simionato Date: Fri, 6 Mar 2026 14:47:50 +1030 Subject: [PATCH 07/13] Update renderer guide --- specification/v0_9/docs/renderer_guide.md | 589 +++++++++++++++------- 1 file changed, 420 insertions(+), 169 deletions(-) diff --git a/specification/v0_9/docs/renderer_guide.md b/specification/v0_9/docs/renderer_guide.md index 7d0d629e8..04aba18f9 100644 --- a/specification/v0_9/docs/renderer_guide.md +++ b/specification/v0_9/docs/renderer_guide.md @@ -1,10 +1,32 @@ -# Unified Data Model Architecture +# Unified Architecture & Implementation Guide -This document describes the architecture of the A2UI client-side data model. The design separates concerns into two distinct primary layers: the language-agnostic **Data Layer** (which parses messages and stores state) and the **Framework-Specific Rendering Layer** (which paints the pixels). +This document describes the architecture of an A2UI client implementation. The design separates concerns into distinct layers to maximize code reuse, ensure memory safety, and provide a streamlined developer experience when adding custom components. -Both of these architectural layers are completely agnostic to the specific components being rendered. Instead, they interact with **Catalogs**. Within a catalog, there is a similar two-layer split: the **Catalog API** defines the schema and interface, while the **Catalog Implementation** defines how to actually render those components in a specific framework. +Both the core data structures and the rendering components are completely agnostic to the specific UI being rendered. Instead, they interact with **Catalogs**. Within a catalog, the implementation follows a 4-layer split: from the pure **Component Schema** down to the **Framework-Specific Adapter** that paints the pixels. -## 1. Data Layer +## Implementation Topologies +Because A2UI spans multiple languages and UI paradigms, the strictness and location of these architectural boundaries will vary depending on the target ecosystem. + +### Dynamic Languages (e.g., TypeScript / JavaScript) +In highly dynamic ecosystems like the web, the architecture is typically split across multiple packages to maximize code reuse across diverse UI frameworks (React, Angular, Vue, Lit). +* **Core Library (`web_core`)**: Implements Layer 1 (Component Schemas) and Layer 2 (The Binder Layer). Because TS/JS has powerful runtime reflection, the core library provides a *Generic Zod Binder* that automatically handles all data binding without framework-specific code. +* **Framework Library (`react_renderer`, `angular_renderer`)**: Implements Layer 3 (Framework-Specific Adapters) and Layer 4 (Strongly-Typed Catalog Implementations). It provides the adapter utilities (`createReactComponent`) and the actual view implementations (the React `Button`, `Text`, etc.). + +### Static Languages (e.g., Kotlin, Swift) +In statically typed languages, runtime reflection is often limited or discouraged for performance reasons. +* **Core Library (e.g., `kotlin_core`)**: Implements Layer 1 (Component Schemas). For Layer 2, the core library typically provides a manually implemented **Binder Layer** for the standard Basic Catalog components. This ensures that even in static environments, basic components have a standardized, framework-agnostic reactive state definition. +* **Code Generation (Future/Optional)**: While the core library starts with manual binders, it may eventually offer **Code Generation** (e.g., KSP, Swift Macros) to automate the creation of Binders for custom components. +* **Custom Components**: In the absence of code generation, developers implementing new, ad-hoc components typically utilize the **"Binderless" Implementation** flow (see Layer 2 Alternative), which allows for direct binding to the data model without intermediate boilerplate. +* **Framework Library (e.g., `compose_renderer`)**: Implements Layer 3 (Adapters) and Layer 4. Uses the predefined Binders to connect to native UI state. + +### Combined Core + Framework Libraries (e.g., Swift + SwiftUI) +In ecosystems dominated by a single UI framework (like iOS with SwiftUI), developers often build a single, unified library rather than splitting Core and Framework into separate packages. +* **Relaxed Boundaries**: The strict separation between Core and Framework libraries can be relaxed. The generic `ComponentContext` and the framework-specific adapter logic are often tightly integrated. +* **Why Keep the Binder Layer?**: Even in a combined library, defining the intermediate **Binder Layer (Layer 2)** remains highly recommended. It standardizes how A2UI data resolves into reactive state (e.g., standardizing the `ComponentBinding` interface). This allows developers adopting the library to easily write alternative implementations of well-known components (e.g., swapping the default SwiftUI Button with a custom corporate-branded SwiftUI Button) without having to rewrite the complex, boilerplate-heavy A2UI data subscription logic. + +--- + +## 1. The Data Layer The Data Layer is responsible for receiving the wire protocol (JSON messages), parsing them, and maintaining a long-lived, mutable state object. This layer follows the exact same design in all programming languages (with minor syntactical variations) and **does not require design work when porting to a new framework**. @@ -17,11 +39,11 @@ It consists of three sub-components: the Processing Layer, the Dumb Models, and To implement the Data Layer effectively, your target environment needs two foundational utilities: a Schema Library and an Observable Library. #### 1. Schema Library -To represent and validate component and function APIs, the Data Layer requires a **Schema Library**. +To represent and validate component and function APIs (Layer 1 of the Catalog), the Data Layer requires a **Schema Library**. -* **Ideal Choice**: A library (like **Zod** in TypeScript or **JsonSchemaBuilder** in Flutter) that allows for programmatic definition of schemas and the ability to validate raw JSON data against those definitions. +* **Ideal Choice**: A library (like **Zod** in TypeScript) that allows for programmatic definition of schemas and the ability to validate raw JSON data against those definitions. * **Capabilities Generation**: The library should ideally support exporting these programmatic definitions to standard JSON Schema for the `getClientCapabilities` payload. -* **Fallback**: If no suitable programmatic library exists for the target language, raw **JSON Schema strings** or manual validation logic can be used instead. +* **Fallback**: If no suitable programmatic library exists for the target language, raw **JSON Schema strings**, `Codable` structs, or `kotlinx.serialization` classes can be used instead. #### 2. Observable Library A2UI relies on a standard observer pattern to reactively update the UI when data changes. The Data Layer and client-side functions must be able to return streams or reactive variables that hold an initial value and emit subsequent updates. @@ -78,143 +100,19 @@ This hierarchy allows a renderer to implement "smart" updates: re-rendering a co * **`DataContext`**: A scoped window into the `DataModel`. Used by functions and components to resolve dependencies and mutate state. * **`ComponentContext`**: A binding object pairing a component with its data scope. -### The Catalog and Component API -The Data Layer relies on a **Catalog** to know which components and functions exist. - -```typescript -interface ComponentApi { - name: string; // Protocol name (e.g. 'Button') - readonly schema: z.ZodType; // Technical definition for capabilities -} - -/** - * Defines a client-side logic handler. - */ -interface FunctionImplementation { - readonly name: string; - readonly description: string; - readonly returnType: 'string' | 'number' | 'boolean' | 'array' | 'object' | 'any' | 'void'; - - /** - * The schema for the arguments this function accepts. - * MUST use the same schema library as the ComponentApi to ensure consistency - * across the catalog. - * This maps directly to the `parameters` field of the `FunctionDefinition` - * in the A2UI client capabilities schema, allowing dynamic capabilities advertising. - */ - readonly schema: z.ZodType; - - /** - * Executes the function logic. - * @param args The key-value pairs of arguments provided in the JSON. - * @param context The DataContext for resolving dependencies and mutating state. - * @returns A synchronous value or a reactive stream (e.g. Observable). - * - * Rationale: The return type here should be flexible based on your language. - * Dynamic languages (like TS/JS) can return a union type (e.g., `unknown | Observable`) - * and let the framework wrap static values. Strictly typed languages (like Swift or Kotlin) - * might instead require this to strictly return their observable equivalent (e.g., `StateFlow`) - * internally wrapping static returns to avoid messy generic union types. - */ - execute(args: Record, context: DataContext): unknown | Observable; -} - -class Catalog { - readonly id: string; // Unique catalog URI - readonly components: ReadonlyMap; - readonly functions?: ReadonlyMap; - - constructor(id: string, components: T[], functions?: FunctionImplementation[]) { - // Initializes the read-only maps - } -} -``` - -#### Function Implementation Rationale -A2UI categorizes client-side functions to balance performance and reactivity. - -**Observability Consistency**: Functions can return either a synchronous literal value (for static results) or a reactive stream (for values that change over time). The execution engine (`DataContext`) is responsible for treating these consistently by wrapping synchronous returns in static observables when evaluating reactively. - -**API Documentation**: Every function MUST include a `schema` using the same schema library selected for the Data Layer. This allows the renderer to validate function arguments at runtime and generate accurate client capabilities (`parameters` in `FunctionDefinition`) for the AI model. - -**Function Categories**: -1. **Pure Logic (Synchronous)**: Functions like `add` or `concat`. Their logic is immediate and depends only on their inputs. They typically return a static primitive value. -2. **External State (Reactive)**: Functions like `clock()` or `networkStatus()`. These return long-lived streams that push updates to the UI independently of data model changes. -3. **Effect Functions**: Side-effect handlers (e.g., `openUrl`, `closeModal`) that return `void`. These are typically triggered by user actions rather than interpolation. - - -### The Processing Layer (`MessageProcessor`) -The **Processing Layer** acts as the "Controller." It accepts the raw stream of A2UI messages (`createSurface`, `updateComponents`, etc.), parses them, and mutates the underlying Data Models accordingly. - -It also handles generating the client capabilities payload via `getClientCapabilities()`. By passing inline catalog definitions to this method, the processor can dynamically generate JSON Schemas for the supported components, allowing the agent to understand the client's available UI components on the fly. - -```typescript -class MessageProcessor { - readonly model: SurfaceGroupModel; // Root state container for all surfaces - - constructor(catalogs: T[], actionHandler: ActionListener); - - processMessages(messages: any[]): void; // Ingests raw JSON message stream - addLifecycleListener(l: SurfaceLifecycleListener): () => void; // Watch for surface lifecycle - getClientCapabilities(options?: CapabilitiesOptions): any; // Generate advertising payload -} -``` - -#### Component Lifecycle: Update vs. Recreate -When processing `updateComponents`, the processor must handle existing IDs carefully: -* **Property Update**: If the component `id` exists and the `type` matches the existing instance, update the `properties` record. This triggers the component's `onUpdated` event. -* **Type Change (Re-creation)**: If the `type` in the message differs from the existing instance's type, the processor MUST remove the old component instance from the model and create a fresh one. This ensures framework renderers correctly reset their internal state and widget types. - - -#### Generating Client Capabilities and Schema Types - -To dynamically generate the `a2uiClientCapabilities` payload (specifically the `inlineCatalogs` array), the renderer needs to convert its internal component schemas into valid JSON Schemas that adhere to the A2UI protocol. - -**Schema Types Location** -The foundational schema types for A2UI components are defined in the `schema` directory (e.g., `renderers/web_core/src/v0_9/schema/common-types.ts`). This is where reusable validation schemas (like Zod definitions) reside. - -**Detectable Common Types** -A2UI heavily relies on shared schema definitions (like `DynamicString`, `DataBinding`, and `Action` from `common_types.json`). However, most schema validation libraries (such as Zod) do not natively support emitting external JSON Schema `$ref` pointers out-of-the-box. - -To solve this, common types must be **detectable** during the JSON Schema conversion process. This is achieved by "tagging" the schemas using their `description` property (e.g., `REF:common_types.json#/$defs/DynamicString`). - -When `getClientCapabilities()` converts the internal schemas: -1. It translates the definition into a raw JSON Schema. -2. It traverses the schema tree looking for string descriptions starting with the `REF:` tag. -3. It strips the tag and replaces the entire node with a valid JSON Schema `$ref` object, preserving any actual developer descriptions using a separator token. -4. It wraps the resulting property schemas in the standard A2UI component envelope (`allOf` containing `ComponentCommon` and the component's `const` type identifier). - -**The Inline Catalogs API** -By passing `{ inlineCatalogs: [myCatalog] }` to `getClientCapabilities()`, the processor: -* Iterates over all the components defined in the provided `catalog`. -* Translates their schemas into the structured format required by the A2UI specification. -* Returns a configuration object ready to be sent in the transport metadata (populating `supportedCatalogIds` and `inlineCatalogs`). - -**Test Cases to Include** -When implementing or modifying the capabilities generator, you must include test cases that verify: -* **Capabilities Generation:** The output successfully includes the `inlineCatalogs` list when requested. -* **Component Envelope:** Generated schemas correctly wrap component properties in an `allOf` block referencing `common_types.json#/$defs/ComponentCommon` and correctly assert the `component` property `const` value. -* **Reference Resolution:** Properties tagged with `REF:` successfully resolve to `$ref` objects instead of expanding the inline schema definition (e.g., a `title` property correctly emits `"$ref": "common_types.json#/$defs/DynamicString"`). -* **Description Preservation:** Additional descriptions appended to tagged types are preserved and properly formatted alongside the reference pointer. - ### The "Dumb" Models -These classes are designed to be "dumb containers" for data. They hold the state of the UI but contain minimal logic. They are organized hierarchically and use a consistent pattern for observability and composition. - -**Key Characteristics:** -* **Mutable:** State is updated in place. -* **Observable:** Each layer is responsible for making its direct properties observable via standard listener patterns, avoiding heavy reactive dependencies. -* **Encapsulated Composition:** Parent layers expose methods to add fully-formed child instances (e.g., `addSurface`, `addComponent`) rather than factory methods that take parameters. +These classes are designed to be "dumb containers" for data. They hold the state of the UI but contain minimal logic. They are organized hierarchically. #### SurfaceGroupModel & SurfaceModel The root containers for active surfaces and their catalogs, data, and components. ```typescript -interface SurfaceLifecycleListener { +interface SurfaceLifecycleListener { onSurfaceCreated?: (s: SurfaceModel) => void; // Called when a new surface is registered onSurfaceDeleted?: (id: string) => void; // Called when a surface is removed } -class SurfaceGroupModel { +class SurfaceGroupModel { addSurface(surface: SurfaceModel): void; deleteSurface(id: string): void; getSurface(id: string): SurfaceModel | undefined; @@ -224,7 +122,7 @@ class SurfaceGroupModel { type ActionListener = (action: any) => void | Promise; // Handler for user interactions -class SurfaceModel { +class SurfaceModel { readonly id: string; ... readonly catalog: T; // Catalog containing framework-specific renderers @@ -346,62 +244,415 @@ While A2UI components are designed to be self-contained, certain rendering logic **Usage**: Component implementations can use `ctx.surfaceComponents` to inspect the metadata of other components in the same surface. > **Guidance**: This pattern is generally discouraged as it increases coupling. Use it only as an essential escape hatch when a framework's layout engine cannot be satisfied by explicit component properties alone. -## 2. Framework Binding Layer -The Framework Binding Layer takes the structured state provided by the Data Layer and translates it into actual UI elements (DOM nodes, Flutter widgets, etc.). This layer provides framework-specific component implementations that extend the data layer's `ComponentApi` to include actual rendering logic. +### The Catalog API -### Key Interfaces and Classes -* **`FrameworkSurface`**: The entrypoint widget for a specific framework. -* **`FrameworkComponent`**: The framework-specific logic for rendering a specific component. +While specific components and frameworks have their own layer definitions (detailed later), the root Data Layer relies on the concept of a **Catalog** to know which components and functions exist during processing. + +A catalog groups these definitions together so the `MessageProcessor` can validate messages and provide capabilities back to the server. -### `FrameworkSurface` -The entrypoint widget for a specific framework. It listens to the `SurfaceModel` to dynamically build the UI tree. It initiates the rendering loop at the component with ID `root`. +```typescript +class Catalog { + readonly id: string; // Unique catalog URI + readonly components: ReadonlyMap; + readonly functions?: ReadonlyMap; + + constructor(id: string, components: T[], functions?: FunctionImplementation[]) { + // Initializes the read-only maps + } +} +``` -### The Rendering Pattern -How components are rendered depends on the target framework's architecture. +### The Processing Layer (`MessageProcessor`) +The **Processing Layer** acts as the "Controller." It accepts the raw stream of A2UI messages (`createSurface`, `updateComponents`, etc.), parses them, and mutates the underlying Data Models accordingly. -#### 1. Functional / Reactive Frameworks (e.g., Flutter, SwiftUI) -In frameworks that use an immutable widget tree, components typically implement a `build` or `render` method that is called whenever the component's properties or bound data change. +It also handles generating the client capabilities payload via `getClientCapabilities()`. By passing inline catalog definitions to this method, the processor can dynamically generate JSON Schemas for the supported components, allowing the agent to understand the client's available UI components on the fly. -**Example Recursive Builder Pattern**: ```typescript -interface MyFrameworkComponent extends ComponentApi { - /** - * @param ctx The component's context. - * @param buildChild A closure provided by the surface to recursively build children. - */ - build(ctx: ComponentContext, buildChild: (id: string) => Widget): Widget; +class MessageProcessor { + readonly model: SurfaceGroupModel; // Root state container for all surfaces + + constructor(catalogs: T[], actionHandler: ActionListener); + + processMessages(messages: any[]): void; // Ingests raw JSON message stream + addLifecycleListener(l: SurfaceLifecycleListener): () => void; // Watch for surface lifecycle + getClientCapabilities(options?: CapabilitiesOptions): any; // Generate advertising payload } ``` -#### 2. Stateful / Imperative Frameworks (e.g., Vanilla DOM, Android Views) -In stateful frameworks, a parent component instance might persist even as its configuration changes. In these cases, the `FrameworkComponent` might maintain a reference to its native element and provide an `update()` method to apply new properties without re-creating the entire child tree. +#### Component Lifecycle: Update vs. Recreate +When processing `updateComponents`, the processor must handle existing IDs carefully: +* **Property Update**: If the component `id` exists and the `type` matches the existing instance, update the `properties` record. This triggers the component's `onUpdated` event. +* **Type Change (Re-creation)**: If the `type` in the message differs from the existing instance's type, the processor MUST remove the old component instance from the model and create a fresh one. This ensures framework renderers correctly reset their internal state and widget types. + +#### Generating Client Capabilities and Schema Types + +To dynamically generate the `a2uiClientCapabilities` payload (specifically the `inlineCatalogs` array), the renderer needs to convert its internal component schemas into valid JSON Schemas that adhere to the A2UI protocol. + +**Schema Types Location** +The foundational schema types for A2UI components are defined in the `schema` directory (e.g., `renderers/web_core/src/v0_9/schema/common-types.ts`). This is where reusable validation schemas (like Zod definitions) reside. + +**Detectable Common Types** +A2UI heavily relies on shared schema definitions (like `DynamicString`, `DataBinding`, and `Action` from `common_types.json`). However, most schema validation libraries (such as Zod) do not natively support emitting external JSON Schema `$ref` pointers out-of-the-box. + +To solve this, common types must be **detectable** during the JSON Schema conversion process. This is achieved by "tagging" the schemas using their `description` property (e.g., `REF:common_types.json#/$defs/DynamicString`). + +When `getClientCapabilities()` converts the internal schemas: +1. It translates the definition into a raw JSON Schema. +2. It traverses the schema tree looking for string descriptions starting with the `REF:` tag. +3. It strips the tag and replaces the entire node with a valid JSON Schema `$ref` object, preserving any actual developer descriptions using a separator token. +4. It wraps the resulting property schemas in the standard A2UI component envelope (`allOf` containing `ComponentCommon` and the component's `const` type identifier). + +--- + +## 2. The Catalog & Component Lifecycle (4-Layer Model) + +How components are rendered depends on the target framework's architecture, but all implementations of A2UI Catalogs follow a standard 4-Layer lifecycle. + +### Layer 1: Component Schema (API Definition) +This layer defines the exact JSON footprint of a component without any rendering or decoding logic. It acts as the single source of truth for the component's contract, exactly mirroring the A2UI component schema definitions. The goal is to define the properties a component accepts (like `label` or `action`) using the platform's preferred schema validation or serialization library. + +#### TypeScript/Web Example +In a web environment, this is typically done using Zod to represent the JSON Schema. + +```typescript +// basic_catalog_api/schemas.ts +export interface ComponentDefinition { + name: string; + schema: PropsSchema; +} + +const ButtonSchema = z.object({ + label: DynamicStringSchema, + action: ActionSchema, +}); -#### Subscription Management -Regardless of the framework paradigm (functional or stateful), the `FrameworkComponent` implementation is responsible for tracking any active subscriptions returned by the `DataContext` or `ComponentModel`. It must ensure these subscriptions are properly disposed of when the component is unmounted from the UI tree. +// Example definition +export const ButtonDef = { + name: "Button" as const, + schema: ButtonSchema +} satisfies ComponentDefinition; +``` + +*Illustrative Examples (Swift/Kotlin):* In Swift, this might be represented by `Codable` structs mapping to the JSON structure. In Kotlin, developers might use `kotlinx.serialization` classes. The choice of serialization library is up to the platform developer, provided it can faithfully represent the A2UI component contract. + +### Layer 2: The Binder Layer (Framework-Agnostic) +A2UI components are heavily reliant on `DynamicValue` bindings, which must be resolved into reactive streams. Framework renderers currently have to manually resolve these, manage context, and handle the lifecycle of data subscriptions. + +The **Binder Layer** absorbs this responsibility. It takes the raw component properties and the `ComponentContext`, and transforms the reactive A2UI bindings into a single, cohesive stream of strongly-typed `ResolvedProps`. + +#### Subscription Lifecycle and Cleanup +A critical responsibility of the Binding is tracking all subscriptions it creates against the underlying data model. As outlined in the Contract of Ownership (see Layer 3), the framework adapter manages the lifecycle of the Binding. When a component is removed from the UI (because its parent was replaced, the surface was deleted, etc.), the framework adapter must call the Binding's `dispose()` method. The Binding then iterates through its internally tracked subscription list and severs them, ensuring that no dangling listeners remain attached to the global `DataModel`. + +#### Generic Interface Concept +Conceptually, the binder layer looks like this in any language: + +```typescript +// The generic Binding interface representing an active connection +export interface ComponentBinding { + // A stateful stream of fully resolved, ready-to-render props. + // It must hold the current value so frameworks can read the initial state synchronously. + readonly propsStream: StatefulStream; // e.g. BehaviorSubject, StateFlow, or CurrentValueSubject + + // Cleans up all underlying data model subscriptions + dispose(): void; +} + +// The Binder definition combining Schema + Binding Logic +// By extending ComponentDefinition, a Binder can be used anywhere a pure schema definition is expected. +export interface ComponentBinder extends ComponentDefinition { + bind(context: ComponentContext): ComponentBinding; +} +``` + +*Note on Stateful Streams:* The `propsStream` should ideally be a stateful stream (common examples include `BehaviorSubject` in RxJS, `StateFlow` in Kotlin Coroutines, or `CurrentValueSubject` in Swift Combine). UI frameworks typically require an initial state to render the first frame synchronously. Because `DataContext.subscribeDynamicValue()` resolves its initial value synchronously, the binder can immediately seed the stream with the fully resolved initial properties. -### Component Traits +#### Generic Binders via Zod (Web Implementation Example) +For TypeScript/Web implementations, one approach is to write a generic `ZodBinder` that automatically infers subscriptions. Instead of writing custom logic for every component, the binder recursively inspects the Zod schema. -#### Reactive Validation (`Checkable`) +When the `ZodBinder` walks the schema and encounters known A2UI dynamic types (e.g., `DynamicStringSchema`), it automatically invokes `context.dataContext.subscribeDynamicValue()`. It stores the returned subscription objects in an internal array. When `dispose()` is called, it loops through this array and unsubscribes them all. + +```typescript +// Illustrative Generic Zod Binding Factory +export function createZodBinding( + schema: T, + context: ComponentContext +): ComponentBinding> { + // 1. Walk the schema to find all DynamicValue and Action properties. + // 2. Map `DynamicValue` properties to `context.dataContext.subscribeDynamicValue()` + // and store the returned `DataSubscription` objects. + // 3. Map `Action` properties to `context.dispatchAction()`. + // 4. Combine all observables (e.g., using `combineLatest` in RxJS) into a single stateful stream. + // 5. Return an object conforming to ComponentBinding whose `dispose()` method unsubscribes all stored subscriptions. + + return new GenericZodBinding(schema, context); +} + +// Button implementation becomes simplified by leveraging the existing ButtonDef: +export const ButtonBinder: ComponentBinder = { + ...ButtonDef, + bind: (ctx) => createZodBinding(ButtonDef.schema, ctx) +}; +``` + +*Note for Static Languages (Swift/Kotlin):* While dynamic runtime reflection is common in web environments, static languages may prefer different strategies. For example, Swift or Kotlin environments might leverage Code Generation (such as Swift Macros or KSP) to generate the boilerplate `Binding` logic based on the schema at compile-time. + +#### Alternative: Binderless Implementation (Direct Binding) +For frameworks that are less dynamic, lack codegen systems, or for developers who simply want to implement a single, one-off component without the abstraction overhead of a generic binder, it is perfectly valid to skip the formal binder layer and implement the component directly. + +In a "binderless" setup, the developer creates the component in one step. The system directly receives the schema, and the render function takes the raw `ComponentContext` (or a lightweight framework-specific wrapper around it), manually subscribing to dynamic properties and returning the native UI element. + +**Dart/Flutter Illustrative Example:** +```dart +// direct_component.dart + +// The developer defines the component in one unified step without a separate binder. +final myButtonComponent = FrameworkComponent( + name: 'Button', + schema: buttonSchema, // A schematic representation of the properties + + // The render function handles reading from context and building the widget. + // It receives the A2UI ComponentContext and a helper to build children. + render: (ComponentContext context, Widget Function(String) buildChild) { + // 1. Manually resolve or subscribe to dynamic values. + // (In Flutter, this might be wrapped in a StreamBuilder or custom hook + // that handles the unsubscription automatically on dispose). + return StreamBuilder( + stream: context.dataContext.observeDynamicValue(context.componentModel.properties['label']), + builder: (context, snapshot) { + return ElevatedButton( + onPressed: () { + context.dispatchAction(context.componentModel.properties['action']); + }, + child: Text(snapshot.data?.toString() ?? ''), + ); + } + ); + } +); +``` +While this approach bypasses the reusable binder layer, it offers a straightforward path for adding custom components and remains fully compliant with the architecture's boundaries. + +### Layer 3: Framework-Specific Adapters +Framework developers should not interact with `ComponentContext` or `ComponentBinding` directly when writing the actual UI view. Instead, the architecture should provide framework-specific adapters that bridge the `Binding`'s stream to the framework's native reactivity and automatically handle the disposal lifecycle to guarantee memory safety. + +#### Contract of Ownership +A crucial part of A2UI's architecture is understanding who "owns" the data layers. +* **The Data Layer (Message Processor) owns the `ComponentModel`**. It creates, updates, and destroys the component's raw data state based on the incoming JSON stream. +* **The Framework Adapter owns the `ComponentContext` and `ComponentBinding`**. When the native framework decides to mount a component onto the screen (e.g., React runs `render`, Flutter runs `build`), the Framework Adapter creates the `ComponentContext` and passes it to the Binder to create a `ComponentBinding`. When the native framework unmounts the component, the Framework Adapter MUST call `binding.dispose()`. + +#### Data Props vs. Structural Props +It's important to distinguish between Data Props (like `label` or `value`) and Structural Props (like `child` or `children`). +* **Data Props:** Handled entirely by the Binder. The adapter receives a stream of fully resolved values (e.g., `"Submit"` instead of a `DynamicString`). +* **Structural Props:** The Binder does not attempt to resolve component IDs into actual UI trees. Instead, it outputs metadata for the children that need to be rendered. + * For a simple `ComponentId` (e.g., `Card.child`), it emits an object like `{ id: string, basePath: string }`. + * For a `ChildList` (e.g., `Column.children`), it evaluates the array and emits a `ChildNode[]` stream. If the `ChildList` is a template, the Binder subscribes to the array in the `DataModel` and maps each item to `{ id: templateId, basePath: '/path/to/array/index' }` (where `index` is the specific index of the item, effectively scoping the child to that specific element). +* The framework adapter is then responsible for taking these node definitions and calling a framework-native `buildChild(id, basePath)` method. + +The adapter acts as a wrapper that: +1. Instantiates the binder (obtaining a `ComponentBinding`). +2. Binds the binding's output stream to the framework's state mechanism. +3. Injects structural rendering helpers (like `buildChild`) alongside the resolved data properties. +4. Passes everything into the developer's view implementation. +5. Hooks into the framework's native destruction lifecycle to call `binding.dispose()`. + +#### React Adapter Illustrative Example +React supports subscribing to external stores directly. An adapter might leverage utilities like `useSyncExternalStore` or `useEffect` to hook into the binding's stream, using the native cleanup mechanisms to dispose of the binding when the component unmounts. It also provides a `buildChild` helper. + +```typescript +// react_adapter.ts +export interface ChildNode { id: string; basePath?: string; } + +export function createReactComponent( + binder: ComponentBinder, + RenderComponent: React.FC<{ props: Resolved, buildChild: (node: ChildNode) => React.ReactNode }> +): ReactComponentRenderer { + return { + name: binder.name, + schema: binder.schema, + render: (ctx: ComponentContext) => { + // Adapter maps `propsStream` into React state. + // One common pattern is registering `binding.dispose()` inside a `useEffect` cleanup block + // so when React unmounts this component, the DataModel subscriptions are severed. + // The wrapper also provides the `buildChild` implementation. + return ; + } + }; +} + +// Usage (Button - Data Props only): +const ReactButton = createReactComponent(ButtonBinder, ({ props }) => ( + +)); + +// Usage (Card - Structural Props): +const ReactCard = createReactComponent(CardBinder, ({ props, buildChild }) => ( +
+ {buildChild(props.child)} +
+)); + +// Usage (Column - ChildList Props): +const ReactColumn = createReactComponent(ColumnBinder, ({ props, buildChild }) => ( +
+ {props.children.map((childNode, index) => ( + + {buildChild(childNode)} + + ))} +
+)); +``` + +#### Angular Adapter Illustrative Example +Angular often utilizes explicit Input bindings and lifecycle hooks. An Angular adapter might take the binding stream and manage updates via `ChangeDetectorRef` or the `AsyncPipe`. + +```typescript +// angular_adapter.ts +export function createAngularComponent( + binder: ComponentBinder, + ComponentClass: Type // The Angular Component Class +): AngularComponentRenderer { + return { + name: binder.name, + schema: binder.schema, + render: (ctx: ComponentContext, viewContainerRef: ViewContainerRef) => { + // 1. Instantiates the Angular Component. + // 2. Creates the binding via binder.bind(ctx). + // 3. Subscribes to `binding.propsStream` and updates component instance inputs. + // 4. Manages change detection. + // 5. Hooks into native destruction (e.g. ngOnDestroy) to call `binding.dispose()`. + return new AngularAdapterWrapper(ctx, binder, ComponentClass, viewContainerRef); + } + }; +} + +// Usage in an app: +@Component({ + selector: 'app-button', + template: `` +}) +export class AngularButtonComponent { + @Input() label: string = ''; + @Input() action: () => void = () => {}; +} + +const NgButton = createAngularComponent(ButtonBinder, AngularButtonComponent); +``` + +#### SwiftUI / Compose Illustrative Concepts +* **SwiftUI:** An adapter might wrap the binding's publisher into an `@ObservedObject` or `@StateObject`. The `dispose()` call could be placed in the `.onDisappear` modifier or within the `deinit` block of the observable object. +* **Jetpack Compose:** An adapter might convert a `StateFlow` to Compose state using utilities like `collectAsState()`. The `dispose()` call could be managed using a `DisposableEffect` keyed on the component instance. + +#### Framework Component Traits + +**Reactive Validation (`Checkable`)** Interactive components that support the `checks` property should implement the `Checkable` trait. * **Aggregate Error Stream**: The component should subscribe to all `CheckRule` conditions defined in its properties. * **UI Feedback**: It should reactively display the `message` of the first failing check as a validation error hint. * **Action Blocking**: Actions (like `Button` clicks) should be reactively disabled or blocked if any validation check in the surface or component fails. -#### Component Subscription Lifecycle -To ensure performance and prevent memory leaks, components MUST strictly manage their subscriptions. Follow these rules: -1. **Lazy Subscription**: Only subscribe to data paths or property updates when the component is actually mounted/attached to the UI. -2. **Path Stability**: If a component's property (e.g., a `value` data path) changes via an `updateComponents` message, the component MUST unsubscribe from the old path before subscribing to the new one. -3. **Destruction / Cleanup**: When a component is removed from the UI (e.g., via a `deleteSurface` message, a conditional render, or when its parent is replaced), the framework binding MUST hook into its native lifecycle (e.g., `ngOnDestroy` in Angular, `dispose` in Flutter, `useEffect` cleanup in React, `onDisappear` in SwiftUI) to trigger unsubscription from all active data and property observables. This ensures listeners are cleared from the `DataModel`. +**Component Subscription Lifecycle Rules** +To ensure performance and prevent memory leaks, framework adapters MUST strictly manage their subscriptions. Follow these rules: +1. **Lazy Subscription**: Only bind and subscribe to data paths or property updates when the component is actually mounted/attached to the UI. +2. **Path Stability**: If a component's property (e.g., a `value` data path) changes via an `updateComponents` message, the adapter/binder MUST unsubscribe from the old path before subscribing to the new one. +3. **Destruction / Cleanup**: As outlined above, when a component is removed from the UI (e.g., via a `deleteSurface` message, a conditional render, or when its parent is replaced), the framework binding MUST hook into its native lifecycle to trigger `binding.dispose()`. This ensures listeners are cleared from the `DataModel`. + +### Layer 4: Strongly-Typed Catalog Implementations +To solve the problem of ensuring all components are properly implemented *and* match the exact API signature, platforms with strong type systems should utilize their advanced typing features (like intersection types in TypeScript or protocols/interfaces in Swift/Kotlin). -## **Basic Catalog Implementation** +This ensures that a provided renderer not only exists, but its `name` and `schema` strictly match the official Catalog Definition, catching mismatches at compile time rather than runtime. -The Standard A2UI Catalog (v0.9) requires a shared logic layer for expression resolution and standard component definitions. To maintain consistency across renderers, implementations should follow this structure: +#### TypeScript Implementation Example +We use TypeScript intersection types to force the framework renderer to intersect with the exact definition. -* **`basic_catalog_api/`**: Contains the framework-agnostic `ComponentApi` definitions for standard components (`Text`, `Button`, `Row`, etc.) and the `FunctionImplementation` definitions for standard functions. -* **`basic_catalog_implementation/`**: Contains the framework-specific rendering logic (e.g. `SwiftUIButton`, `FlutterRow`). +```typescript +// basic_catalog_api/implementation.ts + +// The implementation map forces the framework renderer to intersect with the exact definition +export type BasicCatalogImplementation> = { + Button: TRenderer & { name: "Button", schema: typeof ButtonDef.schema }; + Text: TRenderer & { name: "Text", schema: typeof TextDef.schema }; + Row: TRenderer & { name: "Row", schema: typeof RowDef.schema }; + Column: TRenderer & { name: "Column", schema: typeof ColumnDef.schema }; + // ... all basic components +}; + +// Angular implementation Example +// By extending ComponentDefinition, we ensure the renderer carries the required API metadata +interface AngularComponentRenderer extends ComponentDefinition { + // Angular-specific render method + render: (ctx: ComponentContext, vcr: ViewContainerRef) => any; +} + +export function createAngularBasicCatalog( + implementations: BasicCatalogImplementation +): Catalog { + return new Catalog( + "https://a2ui.org/basic_catalog.json", + Object.values(implementations) + ); +} + +// Usage +const basicCatalog = createAngularBasicCatalog({ + // If NgButton's `name` is not exactly "Button", or if its + // `schema` doesn't match ButtonDef.schema exactly, TypeScript throws an error! + Button: NgButton, + Text: NgText, + Row: NgRow, + Column: NgColumn, + // ... +}); +``` + +--- + +## 3. Basic Catalog Core Functions + +The Standard A2UI Catalog (v0.9) requires a shared logic layer for expression resolution and standard function definitions. + +### Function Definitions +Client-side functions operate similarly to components. They require a definition and an implementation. + +```typescript +/** + * Defines a client-side logic handler. + */ +interface FunctionImplementation { + readonly name: string; + readonly description: string; + readonly returnType: 'string' | 'number' | 'boolean' | 'array' | 'object' | 'any' | 'void'; + + /** + * The schema for the arguments this function accepts. + * MUST use the same schema library as the ComponentApi to ensure consistency + * across the catalog. + * This maps directly to the `parameters` field of the `FunctionDefinition` + * in the A2UI client capabilities schema, allowing dynamic capabilities advertising. + */ + readonly schema: z.ZodType; + + /** + * Executes the function logic. + * @param args The key-value pairs of arguments provided in the JSON. + * @param context The DataContext for resolving dependencies and mutating state. + * @returns A synchronous value or a reactive stream (e.g. Observable). + */ + execute(args: Record, context: DataContext): unknown | Observable; +} +``` + +A2UI categorizes client-side functions to balance performance and reactivity. + +**Observability Consistency**: Functions can return either a synchronous literal value (for static results) or a reactive stream (for values that change over time). The execution engine (`DataContext`) is responsible for treating these consistently by wrapping synchronous returns in static observables when evaluating reactively. + +**Function Categories**: +1. **Pure Logic (Synchronous)**: Functions like `add` or `concat`. Their logic is immediate and depends only on their inputs. They typically return a static primitive value. +2. **External State (Reactive)**: Functions like `clock()` or `networkStatus()`. These return long-lived streams that push updates to the UI independently of data model changes. +3. **Effect Functions**: Side-effect handlers (e.g., `openUrl`, `closeModal`) that return `void`. These are typically triggered by user actions rather than interpolation. -### **Expression Resolution Logic (`formatString`)** +### Expression Resolution Logic (`formatString`) The standard `formatString` function is responsible for interpreting the `${expression}` syntax within string properties. **Implementation Requirements**: From d9f328f5d4f1b46369b250d45f2784276817ba31 Mon Sep 17 00:00:00 2001 From: Jacob Simionato Date: Fri, 6 Mar 2026 15:06:23 +1030 Subject: [PATCH 08/13] Add improvements to the renderer guide --- specification/v0_9/docs/renderer_guide.md | 633 +++++++++++----------- 1 file changed, 310 insertions(+), 323 deletions(-) diff --git a/specification/v0_9/docs/renderer_guide.md b/specification/v0_9/docs/renderer_guide.md index 04aba18f9..b71c8b72f 100644 --- a/specification/v0_9/docs/renderer_guide.md +++ b/specification/v0_9/docs/renderer_guide.md @@ -2,31 +2,31 @@ This document describes the architecture of an A2UI client implementation. The design separates concerns into distinct layers to maximize code reuse, ensure memory safety, and provide a streamlined developer experience when adding custom components. -Both the core data structures and the rendering components are completely agnostic to the specific UI being rendered. Instead, they interact with **Catalogs**. Within a catalog, the implementation follows a 4-layer split: from the pure **Component Schema** down to the **Framework-Specific Adapter** that paints the pixels. +Both the core data structures and the rendering components are completely agnostic to the specific UI being rendered. Instead, they interact with **Catalogs**. Within a catalog, the implementation follows a structured split: from the pure **Component Schema** down to the **Framework-Specific Adapter** that paints the pixels. ## Implementation Topologies Because A2UI spans multiple languages and UI paradigms, the strictness and location of these architectural boundaries will vary depending on the target ecosystem. ### Dynamic Languages (e.g., TypeScript / JavaScript) In highly dynamic ecosystems like the web, the architecture is typically split across multiple packages to maximize code reuse across diverse UI frameworks (React, Angular, Vue, Lit). -* **Core Library (`web_core`)**: Implements Layer 1 (Component Schemas) and Layer 2 (The Binder Layer). Because TS/JS has powerful runtime reflection, the core library provides a *Generic Zod Binder* that automatically handles all data binding without framework-specific code. -* **Framework Library (`react_renderer`, `angular_renderer`)**: Implements Layer 3 (Framework-Specific Adapters) and Layer 4 (Strongly-Typed Catalog Implementations). It provides the adapter utilities (`createReactComponent`) and the actual view implementations (the React `Button`, `Text`, etc.). +* **Core Library (`web_core`)**: Implements the Core Data Layer, Component Schemas, and a Generic Binder Layer. Because TS/JS has powerful runtime reflection, the core library can provide a generic binder that automatically handles all data binding without framework-specific code. +* **Framework Library (`react_renderer`, `angular_renderer`)**: Implements the Framework-Specific Adapters and the actual view implementations (the React `Button`, `Text`, etc.). ### Static Languages (e.g., Kotlin, Swift) In statically typed languages, runtime reflection is often limited or discouraged for performance reasons. -* **Core Library (e.g., `kotlin_core`)**: Implements Layer 1 (Component Schemas). For Layer 2, the core library typically provides a manually implemented **Binder Layer** for the standard Basic Catalog components. This ensures that even in static environments, basic components have a standardized, framework-agnostic reactive state definition. -* **Code Generation (Future/Optional)**: While the core library starts with manual binders, it may eventually offer **Code Generation** (e.g., KSP, Swift Macros) to automate the creation of Binders for custom components. -* **Custom Components**: In the absence of code generation, developers implementing new, ad-hoc components typically utilize the **"Binderless" Implementation** flow (see Layer 2 Alternative), which allows for direct binding to the data model without intermediate boilerplate. -* **Framework Library (e.g., `compose_renderer`)**: Implements Layer 3 (Adapters) and Layer 4. Uses the predefined Binders to connect to native UI state. +* **Core Library (e.g., `kotlin_core`)**: Implements the Core Data Layer and Component Schemas. The core library typically provides a manually implemented **Binder Layer** for the standard Basic Catalog components. This ensures that even in static environments, basic components have a standardized, framework-agnostic reactive state definition. +* **Code Generation (Future/Optional)**: While the core library starts with manual binders, it may eventually offer Code Generation (e.g., KSP, Swift Macros) to automate the creation of Binders for custom components. +* **Custom Components**: In the absence of code generation, developers implementing new, ad-hoc components typically utilize a **"Binderless" Implementation** flow, which allows for direct binding to the data model without intermediate boilerplate. +* **Framework Library (e.g., `compose_renderer`)**: Uses the predefined Binders to connect to native UI state and implements the actual visual components. ### Combined Core + Framework Libraries (e.g., Swift + SwiftUI) In ecosystems dominated by a single UI framework (like iOS with SwiftUI), developers often build a single, unified library rather than splitting Core and Framework into separate packages. * **Relaxed Boundaries**: The strict separation between Core and Framework libraries can be relaxed. The generic `ComponentContext` and the framework-specific adapter logic are often tightly integrated. -* **Why Keep the Binder Layer?**: Even in a combined library, defining the intermediate **Binder Layer (Layer 2)** remains highly recommended. It standardizes how A2UI data resolves into reactive state (e.g., standardizing the `ComponentBinding` interface). This allows developers adopting the library to easily write alternative implementations of well-known components (e.g., swapping the default SwiftUI Button with a custom corporate-branded SwiftUI Button) without having to rewrite the complex, boilerplate-heavy A2UI data subscription logic. +* **Why Keep the Binder Layer?**: Even in a combined library, defining the intermediate Binder Layer remains highly recommended. It standardizes how A2UI data resolves into reactive state. This allows developers adopting the library to easily write alternative implementations of well-known components without having to rewrite the complex, boilerplate-heavy A2UI data subscription logic. --- -## 1. The Data Layer +## 1. The Core Data Layer (Framework Agnostic) The Data Layer is responsible for receiving the wire protocol (JSON messages), parsing them, and maintaining a long-lived, mutable state object. This layer follows the exact same design in all programming languages (with minor syntactical variations) and **does not require design work when porting to a new framework**. @@ -39,9 +39,9 @@ It consists of three sub-components: the Processing Layer, the Dumb Models, and To implement the Data Layer effectively, your target environment needs two foundational utilities: a Schema Library and an Observable Library. #### 1. Schema Library -To represent and validate component and function APIs (Layer 1 of the Catalog), the Data Layer requires a **Schema Library**. +To represent and validate component and function APIs, the Data Layer requires a **Schema Library**. -* **Ideal Choice**: A library (like **Zod** in TypeScript) that allows for programmatic definition of schemas and the ability to validate raw JSON data against those definitions. +* **Ideal Choice**: A library (like **Zod** in TypeScript or **Pydantic** in Python) that allows for programmatic definition of schemas and the ability to validate raw JSON data against those definitions. * **Capabilities Generation**: The library should ideally support exporting these programmatic definitions to standard JSON Schema for the `getClientCapabilities` payload. * **Fallback**: If no suitable programmatic library exists for the target language, raw **JSON Schema strings**, `Codable` structs, or `kotlinx.serialization` classes can be used instead. @@ -62,7 +62,6 @@ To ensure consistency and portability, the Data Layer implementation relies on s #### 1. The "Add" Pattern for Composition We strictly separate **construction** from **composition**. Parent containers do not act as factories for their children. -* **Why?** This decoupling allows the child classes to evolve their constructor signatures without breaking the parent. It also simplifies testing by allowing mock children to be injected easily. * **Pattern:** ```typescript // Parent knows nothing about Child's constructor options @@ -71,16 +70,14 @@ We strictly separate **construction** from **composition**. Parent containers do ``` #### 2. Standard Observer Pattern (Observability) -The models must provide a mechanism for the rendering layer to observe changes. The exact implementation should follow the preferred idioms and libraries of the target language, avoiding heavy reactive dependencies (like RxJS) in the core model. +The models must provide a mechanism for the rendering layer to observe changes. **Principles:** -1. **Low Dependency**: Prefer "lowest common denominator" mechanisms (like simple callbacks or delegates) over complex reactive libraries. +1. **Low Dependency**: Prefer "lowest common denominator" mechanisms over complex reactive libraries. 2. **Multi-Cast**: The mechanism must support multiple listeners registered simultaneously. -3. **Unsubscribe Pattern**: There MUST be a clear way (e.g., returning an "unsubscribe" callback) to stop listening and prevent memory leaks. -4. **Payload Support**: The mechanism must communicate specific data updates (e.g., passing the updated instance) and lifecycle events. -5. **Consistency**: This pattern is used uniformly across `SurfaceGroupModel` (lifecycle), `SurfaceModel` (actions), `SurfaceComponentsModel` (lifecycle), `ComponentModel` (updates), and `DataModel` (data changes). - -In the TypeScript examples, we use a simple `EventSource` pattern with vanilla callbacks. However, other idiomatic approaches—such as `Listenable` properties, `Signals`, or `Streams`—are perfectly acceptable as long as they meet the requirements above. +3. **Unsubscribe Pattern**: There MUST be a clear way to stop listening and prevent memory leaks. +4. **Payload Support**: The mechanism must communicate specific data updates and lifecycle events. +5. **Consistency**: This pattern is used uniformly across the whole state model. #### 3. Granular Reactivity The model is designed to support high-performance rendering through granular updates rather than full-surface refreshes. @@ -88,8 +85,6 @@ The model is designed to support high-performance rendering through granular upd * **Property Changes**: The `ComponentModel` notifies when its specific configuration changes. * **Data Changes**: The `DataModel` notifies only subscribers to the specific path that changed. -This hierarchy allows a renderer to implement "smart" updates: re-rendering a container only when its children list changes, but updating just a specific text node when its bound data value changes. - ### Key Interfaces and Classes * **`MessageProcessor`**: The entry point that ingests raw JSON streams. * **`SurfaceGroupModel`**: The root container for all active surfaces. @@ -245,34 +240,16 @@ While A2UI components are designed to be self-contained, certain rendering logic > **Guidance**: This pattern is generally discouraged as it increases coupling. Use it only as an essential escape hatch when a framework's layout engine cannot be satisfied by explicit component properties alone. -### The Catalog API - -While specific components and frameworks have their own layer definitions (detailed later), the root Data Layer relies on the concept of a **Catalog** to know which components and functions exist during processing. - -A catalog groups these definitions together so the `MessageProcessor` can validate messages and provide capabilities back to the server. - -```typescript -class Catalog { - readonly id: string; // Unique catalog URI - readonly components: ReadonlyMap; - readonly functions?: ReadonlyMap; - - constructor(id: string, components: T[], functions?: FunctionImplementation[]) { - // Initializes the read-only maps - } -} -``` - ### The Processing Layer (`MessageProcessor`) The **Processing Layer** acts as the "Controller." It accepts the raw stream of A2UI messages (`createSurface`, `updateComponents`, etc.), parses them, and mutates the underlying Data Models accordingly. -It also handles generating the client capabilities payload via `getClientCapabilities()`. By passing inline catalog definitions to this method, the processor can dynamically generate JSON Schemas for the supported components, allowing the agent to understand the client's available UI components on the fly. +It also handles generating the client capabilities payload via `getClientCapabilities()`. ```typescript class MessageProcessor { readonly model: SurfaceGroupModel; // Root state container for all surfaces - constructor(catalogs: T[], actionHandler: ActionListener); + constructor(catalogs: Catalog[], actionHandler: ActionListener); processMessages(messages: any[]): void; // Ingests raw JSON message stream addLifecycleListener(l: SurfaceLifecycleListener): () => void; // Watch for surface lifecycle @@ -289,31 +266,70 @@ When processing `updateComponents`, the processor must handle existing IDs caref To dynamically generate the `a2uiClientCapabilities` payload (specifically the `inlineCatalogs` array), the renderer needs to convert its internal component schemas into valid JSON Schemas that adhere to the A2UI protocol. -**Schema Types Location** -The foundational schema types for A2UI components are defined in the `schema` directory (e.g., `renderers/web_core/src/v0_9/schema/common-types.ts`). This is where reusable validation schemas (like Zod definitions) reside. - -**Detectable Common Types** A2UI heavily relies on shared schema definitions (like `DynamicString`, `DataBinding`, and `Action` from `common_types.json`). However, most schema validation libraries (such as Zod) do not natively support emitting external JSON Schema `$ref` pointers out-of-the-box. -To solve this, common types must be **detectable** during the JSON Schema conversion process. This is achieved by "tagging" the schemas using their `description` property (e.g., `REF:common_types.json#/$defs/DynamicString`). +To solve this, common types must be **detectable** during the JSON Schema conversion process. This is often achieved by "tagging" the schemas using their `description` property (e.g., `REF:common_types.json#/$defs/DynamicString`). When `getClientCapabilities()` converts the internal schemas: 1. It translates the definition into a raw JSON Schema. 2. It traverses the schema tree looking for string descriptions starting with the `REF:` tag. -3. It strips the tag and replaces the entire node with a valid JSON Schema `$ref` object, preserving any actual developer descriptions using a separator token. -4. It wraps the resulting property schemas in the standard A2UI component envelope (`allOf` containing `ComponentCommon` and the component's `const` type identifier). +3. It strips the tag and replaces the entire node with a valid JSON Schema `$ref` object. --- -## 2. The Catalog & Component Lifecycle (4-Layer Model) +## 2. Catalog API & Bindings (Framework Agnostic) + +Components and functions in A2UI are organized into **Catalogs**. A catalog defines what components are available to be rendered and what client-side logic can be executed. -How components are rendered depends on the target framework's architecture, but all implementations of A2UI Catalogs follow a standard 4-Layer lifecycle. +### The Catalog API +A catalog groups component definitions (and optionally function definitions) together so the `MessageProcessor` can validate messages and provide capabilities back to the server. + +```typescript +class Catalog { + readonly id: string; // Unique catalog URI (e.g., "https://mycompany.com/catalog.json") + readonly components: ReadonlyMap; + readonly functions?: ReadonlyMap; + + constructor(id: string, components: T[], functions?: FunctionImplementation[]) { + // Initializes the read-only maps + } +} +``` + +### Creating Custom Catalogs +Extensibility is a core feature of A2UI. It should be trivial to create a new catalog by extending an existing one, combining custom components with the standard set. + +*Example of composing a custom catalog:* +```python +# Pseudocode +myCustomCatalog = Catalog( + id="https://mycompany.com/catalogs/custom_catalog.json", + functions=basicCatalog.functions, + components=basicCatalog.components.append([MyCompanyLogoComponent()]) +) +``` ### Layer 1: Component Schema (API Definition) -This layer defines the exact JSON footprint of a component without any rendering or decoding logic. It acts as the single source of truth for the component's contract, exactly mirroring the A2UI component schema definitions. The goal is to define the properties a component accepts (like `label` or `action`) using the platform's preferred schema validation or serialization library. +This layer defines the exact JSON footprint of a component without any rendering logic. It acts as the single source of truth for the component's contract. + +In a statically typed language without an advanced schema reflection library, this might simply be defined as basic interfaces or classes: + +```kotlin +// Simple static definition (Kotlin example) +interface ComponentApi { + val name: String + val schema: Schema // Representing the formal property definition +} + +// In the Core Library, defining the standard component API +abstract class ButtonApi : ComponentApi { + override val name = "Button" + override val schema = ButtonSchema // A constant representing the definition +} +``` -#### TypeScript/Web Example -In a web environment, this is typically done using Zod to represent the JSON Schema. +#### Dynamic Language Optimization (e.g. Zod) +In dynamic languages like TypeScript, we can use tools like Zod to represent the schema and infer types directly from it. ```typescript // basic_catalog_api/schemas.ts @@ -327,362 +343,333 @@ const ButtonSchema = z.object({ action: ActionSchema, }); -// Example definition export const ButtonDef = { name: "Button" as const, schema: ButtonSchema } satisfies ComponentDefinition; ``` -*Illustrative Examples (Swift/Kotlin):* In Swift, this might be represented by `Codable` structs mapping to the JSON structure. In Kotlin, developers might use `kotlinx.serialization` classes. The choice of serialization library is up to the platform developer, provided it can faithfully represent the A2UI component contract. +### Layer 2: The Binder Layer +A2UI components are heavily reliant on `DynamicValue` bindings, which must be resolved into reactive streams. -### Layer 2: The Binder Layer (Framework-Agnostic) -A2UI components are heavily reliant on `DynamicValue` bindings, which must be resolved into reactive streams. Framework renderers currently have to manually resolve these, manage context, and handle the lifecycle of data subscriptions. - -The **Binder Layer** absorbs this responsibility. It takes the raw component properties and the `ComponentContext`, and transforms the reactive A2UI bindings into a single, cohesive stream of strongly-typed `ResolvedProps`. +The **Binder Layer** is a framework-agnostic layer that absorbs this responsibility. It takes the raw component properties and the `ComponentContext`, and transforms the reactive A2UI bindings into a single, cohesive stream of strongly-typed `ResolvedProps`. #### Subscription Lifecycle and Cleanup -A critical responsibility of the Binding is tracking all subscriptions it creates against the underlying data model. As outlined in the Contract of Ownership (see Layer 3), the framework adapter manages the lifecycle of the Binding. When a component is removed from the UI (because its parent was replaced, the surface was deleted, etc.), the framework adapter must call the Binding's `dispose()` method. The Binding then iterates through its internally tracked subscription list and severs them, ensuring that no dangling listeners remain attached to the global `DataModel`. +A critical responsibility of the Binding is tracking all subscriptions it creates against the underlying data model. The framework adapter (Layer 3) manages the lifecycle of the Binding. When a component is removed from the UI, the framework adapter must call the Binding's `dispose()` method. The Binding then iterates through its internally tracked subscription list and severs them, ensuring no dangling listeners remain attached to the global `DataModel`. #### Generic Interface Concept -Conceptually, the binder layer looks like this in any language: ```typescript // The generic Binding interface representing an active connection export interface ComponentBinding { // A stateful stream of fully resolved, ready-to-render props. // It must hold the current value so frameworks can read the initial state synchronously. - readonly propsStream: StatefulStream; // e.g. BehaviorSubject, StateFlow, or CurrentValueSubject + readonly propsStream: StatefulStream; // e.g. BehaviorSubject, StateFlow // Cleans up all underlying data model subscriptions dispose(): void; } // The Binder definition combining Schema + Binding Logic -// By extending ComponentDefinition, a Binder can be used anywhere a pure schema definition is expected. -export interface ComponentBinder extends ComponentDefinition { +export interface ComponentBinder { + readonly name: string; + readonly schema: Schema; // Formal schema for validation and capabilities bind(context: ComponentContext): ComponentBinding; } ``` -*Note on Stateful Streams:* The `propsStream` should ideally be a stateful stream (common examples include `BehaviorSubject` in RxJS, `StateFlow` in Kotlin Coroutines, or `CurrentValueSubject` in Swift Combine). UI frameworks typically require an initial state to render the first frame synchronously. Because `DataContext.subscribeDynamicValue()` resolves its initial value synchronously, the binder can immediately seed the stream with the fully resolved initial properties. - -#### Generic Binders via Zod (Web Implementation Example) -For TypeScript/Web implementations, one approach is to write a generic `ZodBinder` that automatically infers subscriptions. Instead of writing custom logic for every component, the binder recursively inspects the Zod schema. - -When the `ZodBinder` walks the schema and encounters known A2UI dynamic types (e.g., `DynamicStringSchema`), it automatically invokes `context.dataContext.subscribeDynamicValue()`. It stores the returned subscription objects in an internal array. When `dispose()` is called, it loops through this array and unsubscribes them all. +#### Dynamic Language Optimization: Generic Binders +For dynamic languages, you can write a generic factory that automatically inspects the schema and creates all the necessary subscriptions, avoiding the need to write manual binding logic for every single component. ```typescript -// Illustrative Generic Zod Binding Factory -export function createZodBinding( - schema: T, - context: ComponentContext -): ComponentBinding> { - // 1. Walk the schema to find all DynamicValue and Action properties. - // 2. Map `DynamicValue` properties to `context.dataContext.subscribeDynamicValue()` - // and store the returned `DataSubscription` objects. - // 3. Map `Action` properties to `context.dispatchAction()`. - // 4. Combine all observables (e.g., using `combineLatest` in RxJS) into a single stateful stream. - // 5. Return an object conforming to ComponentBinding whose `dispose()` method unsubscribes all stored subscriptions. - - return new GenericZodBinding(schema, context); +// Illustrative Generic Binder Factory +export function createGenericBinding(schema: Schema, context: ComponentContext): ComponentBinding { + // 1. Walk the schema to find all DynamicValue properties. + // 2. Map them to `context.dataContext.subscribeDynamicValue()` + // 3. Store the returned `DataSubscription` objects. + // 4. Combine all observables into a single stateful stream. + // 5. Return a ComponentBinding whose `dispose()` method unsubscribes all stored subscriptions. } - -// Button implementation becomes simplified by leveraging the existing ButtonDef: -export const ButtonBinder: ComponentBinder = { - ...ButtonDef, - bind: (ctx) => createZodBinding(ButtonDef.schema, ctx) -}; ``` -*Note for Static Languages (Swift/Kotlin):* While dynamic runtime reflection is common in web environments, static languages may prefer different strategies. For example, Swift or Kotlin environments might leverage Code Generation (such as Swift Macros or KSP) to generate the boilerplate `Binding` logic based on the schema at compile-time. - #### Alternative: Binderless Implementation (Direct Binding) -For frameworks that are less dynamic, lack codegen systems, or for developers who simply want to implement a single, one-off component without the abstraction overhead of a generic binder, it is perfectly valid to skip the formal binder layer and implement the component directly. - -In a "binderless" setup, the developer creates the component in one step. The system directly receives the schema, and the render function takes the raw `ComponentContext` (or a lightweight framework-specific wrapper around it), manually subscribing to dynamic properties and returning the native UI element. +For frameworks that are less dynamic, lack codegen systems, or for developers who simply want to implement a single, one-off component quickly, it is perfectly valid to skip the formal binder layer and implement the component directly inside the framework adapter. -**Dart/Flutter Illustrative Example:** +*Dart/Flutter Illustrative Example:* ```dart -// direct_component.dart - -// The developer defines the component in one unified step without a separate binder. -final myButtonComponent = FrameworkComponent( - name: 'Button', - schema: buttonSchema, // A schematic representation of the properties - - // The render function handles reading from context and building the widget. - // It receives the A2UI ComponentContext and a helper to build children. - render: (ComponentContext context, Widget Function(String) buildChild) { - // 1. Manually resolve or subscribe to dynamic values. - // (In Flutter, this might be wrapped in a StreamBuilder or custom hook - // that handles the unsubscription automatically on dispose). - return StreamBuilder( - stream: context.dataContext.observeDynamicValue(context.componentModel.properties['label']), - builder: (context, snapshot) { - return ElevatedButton( - onPressed: () { - context.dispatchAction(context.componentModel.properties['action']); - }, - child: Text(snapshot.data?.toString() ?? ''), - ); - } - ); - } -); +// The render function handles reading from context and building the widget manually. +Widget renderButton(ComponentContext context, Widget Function(String) buildChild) { + // Manually observe the dynamic value and manage the stream + return StreamBuilder( + stream: context.dataContext.observeDynamicValue(context.componentModel.properties['label']), + builder: (context, snapshot) { + return ElevatedButton( + onPressed: () { + context.dispatchAction(context.componentModel.properties['action']); + }, + child: Text(snapshot.data?.toString() ?? ''), + ); + } + ); +} ``` -While this approach bypasses the reusable binder layer, it offers a straightforward path for adding custom components and remains fully compliant with the architecture's boundaries. -### Layer 3: Framework-Specific Adapters -Framework developers should not interact with `ComponentContext` or `ComponentBinding` directly when writing the actual UI view. Instead, the architecture should provide framework-specific adapters that bridge the `Binding`'s stream to the framework's native reactivity and automatically handle the disposal lifecycle to guarantee memory safety. +--- -#### Contract of Ownership +## 3. Framework Binding Layer (Framework Specific) + +Framework developers should not interact with raw `ComponentContext` or `ComponentBinding` directly when writing the actual UI views. Instead, the architecture provides framework-specific adapters that bridge the `Binding`'s stream to the framework's native reactivity. + +### Contract of Ownership A crucial part of A2UI's architecture is understanding who "owns" the data layers. * **The Data Layer (Message Processor) owns the `ComponentModel`**. It creates, updates, and destroys the component's raw data state based on the incoming JSON stream. -* **The Framework Adapter owns the `ComponentContext` and `ComponentBinding`**. When the native framework decides to mount a component onto the screen (e.g., React runs `render`, Flutter runs `build`), the Framework Adapter creates the `ComponentContext` and passes it to the Binder to create a `ComponentBinding`. When the native framework unmounts the component, the Framework Adapter MUST call `binding.dispose()`. +* **The Framework Adapter owns the `ComponentContext` and `ComponentBinding`**. When the native framework decides to mount a component onto the screen (e.g., React runs `render`), the Framework Adapter creates the `ComponentContext` and passes it to the Binder. When the native framework unmounts the component, the Framework Adapter MUST call `binding.dispose()`. -#### Data Props vs. Structural Props +### Data Props vs. Structural Props It's important to distinguish between Data Props (like `label` or `value`) and Structural Props (like `child` or `children`). -* **Data Props:** Handled entirely by the Binder. The adapter receives a stream of fully resolved values (e.g., `"Submit"` instead of a `DynamicString`). +* **Data Props:** Handled entirely by the Binder. The adapter receives a stream of fully resolved values (e.g., `"Submit"` instead of a `DynamicString` path). * **Structural Props:** The Binder does not attempt to resolve component IDs into actual UI trees. Instead, it outputs metadata for the children that need to be rendered. * For a simple `ComponentId` (e.g., `Card.child`), it emits an object like `{ id: string, basePath: string }`. - * For a `ChildList` (e.g., `Column.children`), it evaluates the array and emits a `ChildNode[]` stream. If the `ChildList` is a template, the Binder subscribes to the array in the `DataModel` and maps each item to `{ id: templateId, basePath: '/path/to/array/index' }` (where `index` is the specific index of the item, effectively scoping the child to that specific element). -* The framework adapter is then responsible for taking these node definitions and calling a framework-native `buildChild(id, basePath)` method. + * For a `ChildList` (e.g., `Column.children`), it evaluates the array and emits a list of `ChildNode` streams. +* The framework adapter is then responsible for taking these node definitions and calling a framework-native `buildChild(id, basePath)` method recursively. -The adapter acts as a wrapper that: -1. Instantiates the binder (obtaining a `ComponentBinding`). -2. Binds the binding's output stream to the framework's state mechanism. -3. Injects structural rendering helpers (like `buildChild`) alongside the resolved data properties. -4. Passes everything into the developer's view implementation. -5. Hooks into the framework's native destruction lifecycle to call `binding.dispose()`. +### Component Subscription Lifecycle Rules +To ensure performance and prevent memory leaks, framework adapters MUST strictly manage their subscriptions: +1. **Lazy Subscription**: Only bind and subscribe to data paths or property updates when the component is actually mounted/attached to the UI. +2. **Path Stability**: If a component's property changes via an `updateComponents` message, the adapter/binder MUST unsubscribe from the old path before subscribing to the new one. +3. **Destruction / Cleanup**: When a component is removed from the UI (e.g., via a `deleteSurface` message), the framework binding MUST hook into its native lifecycle to trigger `binding.dispose()`. -#### React Adapter Illustrative Example -React supports subscribing to external stores directly. An adapter might leverage utilities like `useSyncExternalStore` or `useEffect` to hook into the binding's stream, using the native cleanup mechanisms to dispose of the binding when the component unmounts. It also provides a `buildChild` helper. +### Reactive Validation (`Checkable`) +Interactive components that support the `checks` property should implement the `Checkable` trait. +* **Aggregate Error Stream**: The component should subscribe to all `CheckRule` conditions defined in its properties. +* **UI Feedback**: It should reactively display the `message` of the first failing check. +* **Action Blocking**: Actions (like `Button` clicks) should be reactively disabled or blocked if any validation check fails. + +### The Happy Path: Developer Experience + +Once the Binder Layer and Framework Adapter are implemented, adding a new UI component becomes extremely simple and strictly type-safe. The developer does not need to worry about JSON pointers, manual subscriptions, or reactive stream lifecycles. They simply receive fully resolved, native types. + +Here is an example of what the "happy path" looks like when implementing a `Button` using a generic React adapter and an existing `ButtonBinder`: ```typescript -// react_adapter.ts -export interface ChildNode { id: string; basePath?: string; } - -export function createReactComponent( - binder: ComponentBinder, - RenderComponent: React.FC<{ props: Resolved, buildChild: (node: ChildNode) => React.ReactNode }> -): ReactComponentRenderer { - return { - name: binder.name, - schema: binder.schema, - render: (ctx: ComponentContext) => { - // Adapter maps `propsStream` into React state. - // One common pattern is registering `binding.dispose()` inside a `useEffect` cleanup block - // so when React unmounts this component, the DataModel subscriptions are severed. - // The wrapper also provides the `buildChild` implementation. - return ; - } - }; +// 1. The framework adapter infers the prop types from the Binder's Schema. +// The raw `DynamicString` label and `Action` object have been automatically +// resolved into a static `string` and a callable `() => void` function. + +// Conceptually, the inferred type looks like this: +interface ButtonResolvedProps { + label?: string; // Resolved from DynamicString + action: () => void; // Resolved from Action + child?: string; // Resolved structural ComponentId } -// Usage (Button - Data Props only): -const ReactButton = createReactComponent(ButtonBinder, ({ props }) => ( - -)); - -// Usage (Card - Structural Props): -const ReactCard = createReactComponent(CardBinder, ({ props, buildChild }) => ( -
- {buildChild(props.child)} -
-)); - -// Usage (Column - ChildList Props): -const ReactColumn = createReactComponent(ColumnBinder, ({ props, buildChild }) => ( -
- {props.children.map((childNode, index) => ( - - {buildChild(childNode)} - - ))} -
-)); +// 2. The developer writes a simple, stateless UI component. +// The `props` argument is strictly typed to match `ButtonResolvedProps`. +const ReactButton = createReactComponent(ButtonBinder, ({ props, buildChild }) => { + return ( + + ); +}); ``` -#### Angular Adapter Illustrative Example -Angular often utilizes explicit Input bindings and lifecycle hooks. An Angular adapter might take the binding stream and manage updates via `ChangeDetectorRef` or the `AsyncPipe`. +Because of the generic types flowing through the adapter, if the developer typos `props.action` as `props.onClick`, or treats `props.label` as an object instead of a string, the compiler will immediately flag a type error. + +### Example: Framework-Specific Adapters +The adapter acts as a wrapper that instantiates the binder, binds its output stream to the framework's state mechanism, injects structural rendering helpers (`buildChild`), and hooks into the native destruction lifecycle to call `dispose()`. + +#### React Pseudo-Adapter ```typescript -// angular_adapter.ts -export function createAngularComponent( - binder: ComponentBinder, - ComponentClass: Type // The Angular Component Class -): AngularComponentRenderer { - return { - name: binder.name, - schema: binder.schema, - render: (ctx: ComponentContext, viewContainerRef: ViewContainerRef) => { - // 1. Instantiates the Angular Component. - // 2. Creates the binding via binder.bind(ctx). - // 3. Subscribes to `binding.propsStream` and updates component instance inputs. - // 4. Manages change detection. - // 5. Hooks into native destruction (e.g. ngOnDestroy) to call `binding.dispose()`. - return new AngularAdapterWrapper(ctx, binder, ComponentClass, viewContainerRef); - } - }; +// Pseudo-code concept for a React adapter +function createReactComponent(binder, RenderComponent) { + return function ReactWrapper({ context, buildChild }) { + // Hook into component mount + const [props, setProps] = useState(binder.initialProps); + + useEffect(() => { + // Create binding on mount + const binding = binder.bind(context); + + // Subscribe to updates + const sub = binding.propsStream.subscribe(newProps => setProps(newProps)); + + // Cleanup on unmount + return () => { + sub.unsubscribe(); + binding.dispose(); + }; + }, [context]); + + return ; + } } +``` -// Usage in an app: +#### Angular Pseudo-Adapter +```typescript +// Pseudo-code concept for an Angular adapter @Component({ - selector: 'app-button', - template: `` + template: ` + + ` }) -export class AngularButtonComponent { - @Input() label: string = ''; - @Input() action: () => void = () => {}; +class AngularWrapper implements OnDestroy, OnInit { + binding: ComponentBinding; + props$: Observable; + + ngOnInit() { + this.binding = this.binder.bind(this.context); + this.props$ = this.binding.propsStream; + } + + ngOnDestroy() { + this.binding.dispose(); // Crucial cleanup + } } - -const NgButton = createAngularComponent(ButtonBinder, AngularButtonComponent); ``` -#### SwiftUI / Compose Illustrative Concepts -* **SwiftUI:** An adapter might wrap the binding's publisher into an `@ObservedObject` or `@StateObject`. The `dispose()` call could be placed in the `.onDisappear` modifier or within the `deinit` block of the observable object. -* **Jetpack Compose:** An adapter might convert a `StateFlow` to Compose state using utilities like `collectAsState()`. The `dispose()` call could be managed using a `DisposableEffect` keyed on the component instance. +--- -#### Framework Component Traits +## 4. Basic Catalog Implementation -**Reactive Validation (`Checkable`)** -Interactive components that support the `checks` property should implement the `Checkable` trait. -* **Aggregate Error Stream**: The component should subscribe to all `CheckRule` conditions defined in its properties. -* **UI Feedback**: It should reactively display the `message` of the first failing check as a validation error hint. -* **Action Blocking**: Actions (like `Button` clicks) should be reactively disabled or blocked if any validation check in the surface or component fails. +Once the core architecture and adapters are built, the actual catalogs can be implemented. -**Component Subscription Lifecycle Rules** -To ensure performance and prevent memory leaks, framework adapters MUST strictly manage their subscriptions. Follow these rules: -1. **Lazy Subscription**: Only bind and subscribe to data paths or property updates when the component is actually mounted/attached to the UI. -2. **Path Stability**: If a component's property (e.g., a `value` data path) changes via an `updateComponents` message, the adapter/binder MUST unsubscribe from the old path before subscribing to the new one. -3. **Destruction / Cleanup**: As outlined above, when a component is removed from the UI (e.g., via a `deleteSurface` message, a conditional render, or when its parent is replaced), the framework binding MUST hook into its native lifecycle to trigger `binding.dispose()`. This ensures listeners are cleared from the `DataModel`. - -### Layer 4: Strongly-Typed Catalog Implementations -To solve the problem of ensuring all components are properly implemented *and* match the exact API signature, platforms with strong type systems should utilize their advanced typing features (like intersection types in TypeScript or protocols/interfaces in Swift/Kotlin). +### Strongly-Typed Catalog Implementations +To ensure all components are properly implemented and match the exact API signature, platforms with strong type systems should utilize their advanced typing features. This ensures that a provided renderer not only exists, but its `name` and `schema` strictly match the official Catalog Definition, catching mismatches at compile time rather than runtime. -This ensures that a provided renderer not only exists, but its `name` and `schema` strictly match the official Catalog Definition, catching mismatches at compile time rather than runtime. +#### Statically Typed Languages (e.g. Kotlin/Swift) +In languages like Kotlin, you can define a strict interface or class that demands concrete instances of the specific component APIs defined by the Core Library. -#### TypeScript Implementation Example -We use TypeScript intersection types to force the framework renderer to intersect with the exact definition. +```kotlin +// The Core Library defines the exact shape of the catalog +class BasicCatalogImplementations( + val button: ButtonApi, // Must be an instance of the ButtonApi class + val text: TextApi, + val row: RowApi + // ... +) -```typescript -// basic_catalog_api/implementation.ts - -// The implementation map forces the framework renderer to intersect with the exact definition -export type BasicCatalogImplementation> = { - Button: TRenderer & { name: "Button", schema: typeof ButtonDef.schema }; - Text: TRenderer & { name: "Text", schema: typeof TextDef.schema }; - Row: TRenderer & { name: "Row", schema: typeof RowDef.schema }; - Column: TRenderer & { name: "Column", schema: typeof ColumnDef.schema }; - // ... all basic components -}; - -// Angular implementation Example -// By extending ComponentDefinition, we ensure the renderer carries the required API metadata -interface AngularComponentRenderer extends ComponentDefinition { - // Angular-specific render method - render: (ctx: ComponentContext, vcr: ViewContainerRef) => any; +// The Framework Adapter implements the native views extending the base APIs +class ComposeButton : ButtonApi() { + // Framework specific render logic } -export function createAngularBasicCatalog( - implementations: BasicCatalogImplementation -): Catalog { - return new Catalog( - "https://a2ui.org/basic_catalog.json", - Object.values(implementations) - ); -} +// The compiler forces all required components to be provided +val implementations = BasicCatalogImplementations( + button = ComposeButton(), + text = ComposeText(), + row = ComposeRow() +) -// Usage -const basicCatalog = createAngularBasicCatalog({ - // If NgButton's `name` is not exactly "Button", or if its - // `schema` doesn't match ButtonDef.schema exactly, TypeScript throws an error! - Button: NgButton, - Text: NgText, - Row: NgRow, - Column: NgColumn, - // ... -}); +val catalog = Catalog("id", listOf(implementations.button, implementations.text, implementations.row)) ``` ---- +#### Dynamic Languages (e.g. TypeScript) +In TypeScript, we can use intersection types to force the framework renderer to intersect with the exact definition. + +```typescript +// Concept: Forcing implementations to match the spec +type BasicCatalogImplementations = { + Button: Renderer & { name: "Button", schema: Schema }, + Text: Renderer & { name: "Text", schema: Schema }, + Row: Renderer & { name: "Row", schema: Schema }, + // ... +}; -## 3. Basic Catalog Core Functions +// If a developer forgets 'Row' or spells it wrong, the compiler throws an error. +const catalog = new Catalog("id", [ + implementations.Button, + implementations.Text, + implementations.Row +]); +``` -The Standard A2UI Catalog (v0.9) requires a shared logic layer for expression resolution and standard function definitions. +### Basic Catalog Core Functions +The Standard A2UI Catalog requires a shared logic layer for standard function definitions (like `length`, `formatDate`, etc.). -### Function Definitions -Client-side functions operate similarly to components. They require a definition and an implementation. +#### Function Definitions +Client-side functions operate similarly to components. They require a definition (schema) and an implementation. ```typescript -/** - * Defines a client-side logic handler. - */ interface FunctionImplementation { readonly name: string; - readonly description: string; readonly returnType: 'string' | 'number' | 'boolean' | 'array' | 'object' | 'any' | 'void'; + readonly schema: Schema; // The expected arguments - /** - * The schema for the arguments this function accepts. - * MUST use the same schema library as the ComponentApi to ensure consistency - * across the catalog. - * This maps directly to the `parameters` field of the `FunctionDefinition` - * in the A2UI client capabilities schema, allowing dynamic capabilities advertising. - */ - readonly schema: z.ZodType; - - /** - * Executes the function logic. - * @param args The key-value pairs of arguments provided in the JSON. - * @param context The DataContext for resolving dependencies and mutating state. - * @returns A synchronous value or a reactive stream (e.g. Observable). - */ + // Executes the function logic. Returns a value or a reactive stream. execute(args: Record, context: DataContext): unknown | Observable; } ``` -A2UI categorizes client-side functions to balance performance and reactivity. - -**Observability Consistency**: Functions can return either a synchronous literal value (for static results) or a reactive stream (for values that change over time). The execution engine (`DataContext`) is responsible for treating these consistently by wrapping synchronous returns in static observables when evaluating reactively. - -**Function Categories**: -1. **Pure Logic (Synchronous)**: Functions like `add` or `concat`. Their logic is immediate and depends only on their inputs. They typically return a static primitive value. -2. **External State (Reactive)**: Functions like `clock()` or `networkStatus()`. These return long-lived streams that push updates to the UI independently of data model changes. -3. **Effect Functions**: Side-effect handlers (e.g., `openUrl`, `closeModal`) that return `void`. These are typically triggered by user actions rather than interpolation. - -### Expression Resolution Logic (`formatString`) -The standard `formatString` function is responsible for interpreting the `${expression}` syntax within string properties. +#### Expression Resolution Logic (`formatString`) +The standard `formatString` function is uniquely complex. It is responsible for interpreting the `${expression}` syntax within string properties. **Implementation Requirements**: 1. **Recursion**: The function implementation MUST use `DataContext.resolveDynamicValue()` or `DataContext.subscribeDynamicValue()` to recursively evaluate nested expressions or function calls (e.g., `${formatDate(value:${/date})}`). -2. **Tokenization**: The parser must distinguish between: - * **DataPath**: A raw JSON Pointer (e.g., `${/user/name}`). - * **FunctionCall**: Identified by parentheses (e.g., `${now()}`). +2. **Tokenization**: The parser must distinguish between DataPaths (e.g., `${/user/name}`) and FunctionCalls (e.g., `${now()}`). 3. **Escaping**: Literal `${` sequences must be handled (typically by escaping as `\${`). -4. **Reactive Coercion**: Results are transformed into strings using the **Type Coercion Standards** defined in the Data Layer section. - -## Resources - -When implementing a new rendering framework, you should definitely read the core JSON schema files for the protocol and the markdown doc in the specification. Here are the key resources: - -* **A2UI Protocol Specification:** - * `specification/v0_9/docs/a2ui_protocol.md` - * [GitHub Link](https://github.com/google/A2UI/tree/main/specification/v0_9/docs/a2ui_protocol.md) -* **JSON Schemas:** (Core files for the protocol) - * `a2ui_client_capabilities.json`: [`specification/v0_9/json/a2ui_client_capabilities.json`](https://github.com/google/A2UI/tree/main/specification/v0_9/json/a2ui_client_capabilities.json) - * `a2ui_client_data_model.json`: [`specification/v0_9/json/a2ui_client_data_model.json`](https://github.com/google/A2UI/tree/main/specification/v0_9/json/a2ui_client_data_model.json) - * `basic_catalog.json`: [`specification/v0_9/json/basic_catalog.json`](https://github.com/google/A2UI/tree/main/specification/v0_9/json/basic_catalog.json) - * `basic_catalog_rules.txt`: [`specification/v0_9/json/basic_catalog_rules.txt`](https://github.com/google/A2UI/tree/main/specification/v0_9/json/basic_catalog_rules.txt) - * `client_to_server.json`: [`specification/v0_9/json/client_to_server.json`](https://github.com/google/A2UI/tree/main/specification/v0_9/json/client_to_server.json) - * `client_to_server_list.json`: [`specification/v0_9/json/client_to_server_list.json`](https://github.com/google/A2UI/tree/main/specification/v0_9/json/client_to_server_list.json) - * `common_types.json`: [`specification/v0_9/json/common_types.json`](https://github.com/google/A2UI/tree/main/specification/v0_9/json/common_types.json) - * `server_to_client.json`: [`specification/v0_9/json/server_to_client.json`](https://github.com/google/A2UI/tree/main/specification/v0_9/json/server_to_client.json) - * `server_to_client_list.json`: [`specification/v0_9/json/server_to_client_list.json`](https://github.com/google/A2UI/tree/main/specification/v0_9/json/server_to_client_list.json) -* **Web Core Reference Implementation:** - * `renderers/web_core/src/v0_9/` - * [GitHub Link](https://github.com/google/A2UI/tree/main/renderers/web_core/src/v0_9) -* **Flutter Implementation:** - * The Flutter renderer is maintained in a separate repository. - * [GitHub Link](https://github.com/flutter/genui/tree/main/packages/genui) \ No newline at end of file +4. **Reactive Coercion**: Results are transformed into strings using the Type Coercion Standards. + +--- + +## 5. Agent Implementation Guide + +If you are an AI Agent tasked with building a new renderer for A2UI, you MUST follow this strict, phased sequence of operations. Do not attempt to implement the entire architecture at once. + +### 1. Context to Ingest +Before writing any code, thoroughly review: +* `specification/v0_9/docs/a2ui_protocol.md` (for protocol rules) +* `specification/v0_9/json/common_types.json` (for dynamic binding types) +* `specification/v0_9/json/server_to_client.json` (for message envelopes) +* `specification/v0_9/json/catalogs/minimal/minimal_catalog.json` (your initial target) + +### 2. Key Dependency Decisions +Create a plan document explicitly stating: +* Which **Schema Library** you will use (or if you will use raw language constructs like `structs`/`data classes`). +* Which **Observable/Reactive Library** you will use (must support multi-cast and clear unsubscription). +* Which native UI framework you are targeting. + +### 3. Core Model Layer +Implement the framework-agnostic Data Layer (Section 1). +* Implement standard listener patterns (`EventSource`/`EventEmitter`). +* Implement `DataModel`, ensuring correct JSON pointer resolution and the cascade/bubble notification strategy. +* Implement `ComponentModel`, `SurfaceComponentsModel`, `SurfaceModel`, and `SurfaceGroupModel`. +* Implement `DataContext` and `ComponentContext`. +* Implement `MessageProcessor`. Include logic for detecting schema references to generate `ClientCapabilities`. +* Define the `Catalog`, `ComponentApi`, and `FunctionImplementation` interfaces. +* Define the `ComponentBinding` interface. + +### 4. Framework-Specific Layer +Implement the bridge between the agnostic models and the native UI (Section 3). +* Define the `ComponentAdapter` API (how the core library hands off a component to the framework). +* Implement the mechanism that binds a `ComponentBinding` stream to the native UI state (e.g., a wrapper wrapper view/widget). +* Implement the recursive `Surface` builder that takes a `surfaceId`, finds the "root" component, and recursively calls `buildChild`. +* **Crucial**: Ensure the unmount/dispose lifecycle hook calls `binding.dispose()`. + +### 5. Minimal Catalog Support +Do not start with the full Basic Catalog. Target the `minimal_catalog.json` first. +* **Core Library**: Create definitions/binders for `Text`, `Row`, `Column`, `Button`, and `TextField`. +* **Core Library**: Implement the `capitalize` function. +* **Framework Library**: Implement the actual native UI widgets for these 5 components. +* Write a `createMinimalCatalog()` function that bundles these together. + +### 6. Demo Application (Milestone) +Build a self-contained application to prove the architecture works before scaling. +* The app should run entirely locally (no server required). +* It should load the JSON message arrays from `specification/v0_9/json/catalogs/minimal/examples/`. +* It should display a list of these examples. +* When an example is selected, it should pipe the messages into the `MessageProcessor` and render the surface. +* **Reactivity Test**: Add a mechanism to simulate delayed `updateDataModel` messages (e.g., waiting 2 seconds before sending data) to prove that the UI progressively renders and reacts to changes. + +**STOP HERE. Ask the human user for approval of the architecture and demo application before proceeding to step 7.** + +### 7. Basic Catalog Support +Once the minimal architecture is proven robust: +* **Core Library**: Implement the full suite of basic functions (including the complex `formatString` parser). +* **Core Library**: Create definitions/binders for the remaining Basic Catalog components. +* **Framework Library**: Implement all remaining UI widgets. +* **Tests**: Look at existing reference implementations (e.g., `web_core`) to formulate and run comprehensive test cases for data coercion and function logic. +* Update the Demo App to load samples from `specification/v0_9/json/catalogs/basic/examples/`. From a4b2b614d21b2f75e032a8438b1a4ea35f2ce97d Mon Sep 17 00:00:00 2001 From: Jacob Simionato Date: Fri, 6 Mar 2026 15:07:16 +1030 Subject: [PATCH 09/13] Delete catalog proposal --- .../v0_9/docs/catalog_api_proposal.md | 400 ------------------ 1 file changed, 400 deletions(-) delete mode 100644 specification/v0_9/docs/catalog_api_proposal.md diff --git a/specification/v0_9/docs/catalog_api_proposal.md b/specification/v0_9/docs/catalog_api_proposal.md deleted file mode 100644 index 2e262dc03..000000000 --- a/specification/v0_9/docs/catalog_api_proposal.md +++ /dev/null @@ -1,400 +0,0 @@ -# A2UI Catalog API Architecture Proposal (v0.9) - -## Objective -To refine the A2UI client-side Catalog API to clearly separate component schema definitions, data decoding logic, and framework-specific rendering. This will improve code sharing across frameworks, enhance type safety when implementing catalogs, and provide a streamlined, idiomatic developer experience for creating custom components. - -## Use Cases -This architecture is designed to support the following scenarios when working with Catalogs: -- Declare a Component or Catalog with a fixed API which could have multiple implementations. -- Implement a Component from scratch with a new API and new one-off implementation -- Implement a Component based on an API which is defined elsewhere -- Implement a Component using an API and a binder layer already implemented, and potentially shared across different UI frameworks for the same language -- Implement an entire Catalog to match a predefined Catalog API, with type safety to ensure I include all the correct components and schemas - -## Requirements Addressed - -1. **Share Component Schemas**: Allow developers to declare component schemas independently so they can be reused across different platform implementations. -2. **Share Decoding Logic**: Centralize the boilerplate of resolving `DynamicValue` properties, handling reactive streams, and tracking subscriptions into a framework-agnostic "Binder" layer. Provide details on how a generic binder can be built using Zod for web environments. -3. **Reliable Catalog Implementation**: Provide a strongly-typed mechanism to ensure a specific framework implementation covers all required components of a defined catalog (e.g., ensuring `createAngularBasicCatalog` includes a renderer for `Button`, `Text`, etc.). -4. **Framework-Specific Adapters**: Provide idiomatic APIs for specific frameworks (e.g., a React adapter and an Angular adapter that provide standard framework props/inputs instead of raw `ComponentContext`). -5. **Streamlined DX (Stretch Goal)**: Explore a `defineCatalogImplementation` API for defining catalogs quickly with high type safety. - -## Implementation Topologies -Because A2UI spans multiple languages and UI paradigms, the strictness and location of these architectural boundaries will vary depending on the target ecosystem. - -### Dynamic Languages (e.g., TypeScript / JavaScript) -In highly dynamic ecosystems like the web, the architecture is typically split across multiple packages to maximize code reuse across diverse UI frameworks (React, Angular, Vue, Lit). -* **Core Library (`web_core`)**: Implements Layer 1 (Component Schemas) and Layer 2 (The Binder Layer). Because TS/JS has powerful runtime reflection, the core library provides a *Generic Zod Binder* that automatically handles all data binding without framework-specific code. -* **Framework Library (`react_renderer`, `angular_renderer`)**: Implements Layer 3 (Framework-Specific Adapters) and Layer 4 (Strongly-Typed Catalog Implementations). It provides the adapter utilities (`createReactComponent`) and the actual view implementations (the React `Button`, `Text`, etc.). - -### Static Languages (e.g., Kotlin, Swift) -In statically typed languages, runtime reflection is often limited or discouraged for performance reasons. -* **Core Library (e.g., `kotlin_core`)**: Implements Layer 1 (Component Schemas). For Layer 2, the core library typically provides a manually implemented **Binder Layer** for the standard Basic Catalog components. This ensures that even in static environments, basic components have a standardized, framework-agnostic reactive state definition. -* **Code Generation (Future/Optional)**: While the core library starts with manual binders, it may eventually offer **Code Generation** (e.g., KSP, Swift Macros) to automate the creation of Binders for custom components. -* **Custom Components**: In the absence of code generation, developers implementing new, ad-hoc components typically utilize the **"Binderless" Implementation** flow (see Layer 2 Alternative), which allows for direct binding to the data model without intermediate boilerplate. -* **Framework Library (e.g., `compose_renderer`)**: Implements Layer 3 (Adapters) and Layer 4. Uses the predefined Binders to connect to native UI state. - -### Combined Core + Framework Libraries (e.g., Swift + SwiftUI) -In ecosystems dominated by a single UI framework (like iOS with SwiftUI), developers often build a single, unified library rather than splitting Core and Framework into separate packages. -* **Relaxed Boundaries**: The strict separation between Core and Framework libraries can be relaxed. The generic `ComponentContext` and the framework-specific adapter logic are often tightly integrated. -* **Why Keep the Binder Layer?**: Even in a combined library, defining the intermediate **Binder Layer (Layer 2)** remains highly recommended. It standardizes how A2UI data resolves into reactive state (e.g., standardizing the `ComponentBinding` interface). This allows developers adopting the library to easily write alternative implementations of well-known components (e.g., swapping the default SwiftUI Button with a custom corporate-branded SwiftUI Button) without having to rewrite the complex, boilerplate-heavy A2UI data subscription logic. - ---- - -## Proposed Architecture: The 4-Layer Model - -We propose breaking down the component lifecycle into four distinct layers: - -### 1. Component Schema (API Definition) -This layer defines the exact JSON footprint of a component without any rendering or decoding logic. It acts as the single source of truth for the component's contract, exactly mirroring the A2UI component schema definitions. The goal is to define the properties a component accepts (like `label` or `action`) using the platform's preferred schema validation or serialization library. - -#### TypeScript/Web Example -In a web environment, this is typically done using Zod to represent the JSON Schema. - -```typescript -// basic_catalog_api/schemas.ts -export interface ComponentDefinition { - name: string; - schema: PropsSchema; -} - -const ButtonSchema = z.object({ - label: DynamicStringSchema, - action: ActionSchema, -}); - -// Example definition -export const ButtonDef = { - name: "Button" as const, - schema: ButtonSchema -} satisfies ComponentDefinition; -``` - -*Note for Swift/Kotlin:* In Swift, this would be represented by `Codable` structs mapping to the JSON structure. In Kotlin, this would be `kotlinx.serialization` classes. The architectural separation of concerns remains identical. - -### 2. The Binder Layer (Framework-Agnostic) -A2UI components are heavily reliant on `DynamicValue` bindings, which must be resolved into reactive streams. Framework renderers currently have to manually resolve these, manage context, and handle the lifecycle of data subscriptions. - -The **Binder Layer** absorbs this responsibility. It takes the raw component properties and the `ComponentContext`, and transforms the reactive A2UI bindings into a single, cohesive stream of strongly-typed `ResolvedProps`. - -#### Subscription Lifecycle and Cleanup -A critical responsibility of the Binding is tracking all subscriptions it creates against the underlying data model. As outlined in the Contract of Ownership (see Framework-Specific Adapters), the framework adapter manages the lifecycle of the Binding. When a component is removed from the UI (because its parent was replaced, the surface was deleted, etc.), the framework adapter must call the Binding's `dispose()` method. The Binding then iterates through its internally tracked subscription list and severs them, ensuring that no dangling listeners remain attached to the global `DataModel`. - -#### Generic Interface Concept -Conceptually, the binder layer looks like this in any language: - -```typescript -// The generic Binding interface representing an active connection -export interface ComponentBinding { - // A stateful stream of fully resolved, ready-to-render props. - // It must hold the current value so frameworks can read the initial state synchronously. - readonly propsStream: StatefulStream; // e.g. BehaviorSubject, StateFlow, or CurrentValueSubject - - // Cleans up all underlying data model subscriptions - dispose(): void; -} - -// The Binder definition combining Schema + Binding Logic -// By extending ComponentDefinition, a Binder can be used anywhere a pure schema definition is expected. -export interface ComponentBinder extends ComponentDefinition { - bind(context: ComponentContext): ComponentBinding; -} -``` - -*Note on Stateful Streams:* The `propsStream` should ideally be a stateful stream (common examples include `BehaviorSubject` in RxJS, `StateFlow` in Kotlin Coroutines, or `CurrentValueSubject` in Swift Combine). UI frameworks typically require an initial state to render the first frame synchronously. Because `DataContext.subscribeDynamicValue()` resolves its initial value synchronously, the binder can immediately seed the stream with the fully resolved initial properties. - -#### Generic Binders via Zod (Web Implementation Example) -For TypeScript/Web implementations, one approach is to write a generic `ZodBinder` that automatically infers subscriptions. Instead of writing custom logic for every component, the binder recursively inspects the Zod schema. - -When the `ZodBinder` walks the schema and encounters known A2UI dynamic types (e.g., `DynamicStringSchema`), it automatically invokes `context.dataContext.subscribeDynamicValue()`. It stores the returned subscription objects in an internal array. When `dispose()` is called, it loops through this array and unsubscribes them all. - -```typescript -// Illustrative Generic Zod Binding Factory -export function createZodBinding( - schema: T, - context: ComponentContext -): ComponentBinding> { - // 1. Walk the schema to find all DynamicValue and Action properties. - // 2. Map `DynamicValue` properties to `context.dataContext.subscribeDynamicValue()` - // and store the returned `DataSubscription` objects. - // 3. Map `Action` properties to `context.dispatchAction()`. - // 4. Combine all observables (e.g., using `combineLatest` in RxJS) into a single stateful stream. - // 5. Return an object conforming to ComponentBinding whose `dispose()` method unsubscribes all stored subscriptions. - - return new GenericZodBinding(schema, context); -} - -// Button implementation becomes simplified by leveraging the existing ButtonDef: -export const ButtonBinder: ComponentBinder = { - ...ButtonDef, - bind: (ctx) => createZodBinding(ButtonDef.schema, ctx) -}; -``` - -*Note for Static Languages (Swift/Kotlin):* Dynamic runtime reflection isn't as easily feasible. Swift/Kotlin environments can rely on Code Generation (Swift Macros, KSP) to generate the boilerplate `Binding` logic based on the schema at compile-time. - -#### Alternative: Binderless Implementation (Direct Binding) -For frameworks that are less dynamic, lack codegen systems, or for developers who simply want to implement a single, one-off component without the abstraction overhead of a generic binder, it is perfectly valid to skip the formal binder layer and implement the component directly. - -In a "binderless" setup, the developer creates the component in one step. The system directly receives the schema, and the render function takes the raw `ComponentContext` (or a lightweight framework-specific wrapper around it), manually subscribing to dynamic properties and returning the native UI element. - -**Dart/Flutter Illustrative Example:** -```dart -// direct_component.dart - -// The developer defines the component in one unified step without a separate binder. -final myButtonComponent = FrameworkComponent( - name: 'Button', - schema: buttonSchema, // A schematic representation of the properties - - // The render function handles reading from context and building the widget. - // It receives the A2UI ComponentContext and a helper to build children. - render: (ComponentContext context, Widget Function(String) buildChild) { - // 1. Manually resolve or subscribe to dynamic values. - // (In Flutter, this might be wrapped in a StreamBuilder or custom hook - // that handles the unsubscription automatically on dispose). - return StreamBuilder( - stream: context.dataContext.observeDynamicValue(context.componentModel.properties['label']), - builder: (context, snapshot) { - return ElevatedButton( - onPressed: () { - context.dispatchAction(context.componentModel.properties['action']); - }, - child: Text(snapshot.data?.toString() ?? ''), - ); - } - ); - } -); -``` -While this approach bypasses the reusable binder layer, it offers a straightforward path for adding custom components and remains fully compliant with the architecture's boundaries. - -### 3. Framework-Specific Adapters -Framework developers should not interact with `ComponentContext` or `ComponentBinding` directly when writing the actual UI view. Instead, the architecture should provide framework-specific adapters that bridge the `Binding`'s stream to the framework's native reactivity and automatically handle the disposal lifecycle to guarantee memory safety. - -#### Contract of Ownership -A crucial part of A2UI's architecture is understanding who "owns" the data layers. -* **The Data Layer (Message Processor) owns the `ComponentModel`**. It creates, updates, and destroys the component's raw data state based on the incoming JSON stream. -* **The Framework Adapter owns the `ComponentContext` and `ComponentBinding`**. When the native framework decides to mount a component onto the screen (e.g., React runs `render`, Flutter runs `build`), the Framework Adapter creates the `ComponentContext` and passes it to the Binder to create a `ComponentBinding`. When the native framework unmounts the component, the Framework Adapter MUST call `binding.dispose()`. - -#### Data Props vs. Structural Props -It's important to distinguish between Data Props (like `label` or `value`) and Structural Props (like `child` or `children`). -* **Data Props:** Handled entirely by the Binder. The adapter receives a stream of fully resolved values (e.g., `"Submit"` instead of a `DynamicString`). -* **Structural Props:** The Binder does not attempt to resolve component IDs into actual UI trees. Instead, it outputs metadata for the children that need to be rendered. - * For a simple `ComponentId` (e.g., `Card.child`), it emits an object like `{ id: string, basePath: string }`. - * For a `ChildList` (e.g., `Column.children`), it evaluates the array and emits a `ChildNode[]` stream. If the `ChildList` is a template, the Binder subscribes to the array in the `DataModel` and maps each item to `{ id: templateId, basePath: '/path/to/item/index' }`. -* The framework adapter is then responsible for taking these node definitions and calling a framework-native `buildChild(id, basePath)` method. - -The adapter acts as a wrapper that: -1. Instantiates the binder (obtaining a `ComponentBinding`). -2. Binds the binding's output stream to the framework's state mechanism. -3. Injects structural rendering helpers (like `buildChild`) alongside the resolved data properties. -4. Passes everything into the developer's view implementation. -5. Hooks into the framework's native destruction lifecycle to call `binding.dispose()`. - -#### React Adapter Illustrative Example -React supports subscribing to external stores directly. An adapter might leverage utilities like `useSyncExternalStore` or `useEffect` to hook into the binding's stream, using the native cleanup mechanisms to dispose of the binding when the component unmounts. It also provides a `buildChild` helper. - -```typescript -// react_adapter.ts -export interface ChildNode { id: string; basePath?: string; } - -export function createReactComponent( - binder: ComponentBinder, - RenderComponent: React.FC<{ props: Resolved, buildChild: (node: ChildNode) => React.ReactNode }> -): ReactComponentRenderer { - return { - name: binder.name, - schema: binder.schema, - render: (ctx: ComponentContext) => { - // Adapter maps `propsStream` into React state. - // One common pattern is registering `binding.dispose()` inside a `useEffect` cleanup block - // so when React unmounts this component, the DataModel subscriptions are severed. - // The wrapper also provides the `buildChild` implementation. - return ; - } - }; -} - -// Usage (Button - Data Props only): -const ReactButton = createReactComponent(ButtonBinder, ({ props }) => ( - -)); - -// Usage (Card - Structural Props): -const ReactCard = createReactComponent(CardBinder, ({ props, buildChild }) => ( -
- {buildChild(props.child)} -
-)); - -// Usage (Column - ChildList Props): -const ReactColumn = createReactComponent(ColumnBinder, ({ props, buildChild }) => ( -
- {props.children.map((childNode, index) => ( - - {buildChild(childNode)} - - ))} -
-)); -``` - -#### Angular Adapter Illustrative Example -Angular often utilizes explicit Input bindings and lifecycle hooks. An Angular adapter might take the binding stream and manage updates via `ChangeDetectorRef` or the `AsyncPipe`. - -```typescript -// angular_adapter.ts -export function createAngularComponent( - binder: ComponentBinder, - ComponentClass: Type // The Angular Component Class -): AngularComponentRenderer { - return { - name: binder.name, - schema: binder.schema, - render: (ctx: ComponentContext, viewContainerRef: ViewContainerRef) => { - // 1. Instantiates the Angular Component. - // 2. Creates the binding via binder.bind(ctx). - // 3. Subscribes to `binding.propsStream` and updates component instance inputs. - // 4. Manages change detection. - // 5. Hooks into native destruction (e.g. ngOnDestroy) to call `binding.dispose()`. - return new AngularAdapterWrapper(ctx, binder, ComponentClass, viewContainerRef); - } - }; -} - -// Usage in an app: -@Component({ - selector: 'app-button', - template: `` -}) -export class AngularButtonComponent { - @Input() label: string = ''; - @Input() action: () => void = () => {}; -} - -const NgButton = createAngularComponent(ButtonBinder, AngularButtonComponent); -``` - -#### SwiftUI / Compose Illustrative Concepts -* **SwiftUI:** An adapter might wrap the binding's publisher into an `@ObservedObject` or `@StateObject`. The `dispose()` call could be placed in the `.onDisappear` modifier or within the `deinit` block of the observable object. -* **Jetpack Compose:** An adapter might convert a `StateFlow` to Compose state using utilities like `collectAsState()`. The `dispose()` call could be managed using a `DisposableEffect` keyed on the component instance. - -### 4. Strongly-Typed Catalog Implementations -To solve the problem of ensuring all components are properly implemented *and* match the exact API signature, platforms with strong type systems should utilize their advanced typing features (like intersection types in TypeScript or protocols/interfaces in Swift/Kotlin). - -This ensures that a provided renderer not only exists, but its `name` and `schema` strictly match the official Catalog Definition, catching mismatches at compile time rather than runtime. - -#### TypeScript Implementation Example -We use TypeScript intersection types to force the framework renderer to intersect with the exact definition. - -```typescript -// basic_catalog_api/implementation.ts - -// The implementation map forces the framework renderer to intersect with the exact definition -export type BasicCatalogImplementation> = { - Button: TRenderer & { name: "Button", schema: typeof ButtonDef.schema }; - Text: TRenderer & { name: "Text", schema: typeof TextDef.schema }; - Row: TRenderer & { name: "Row", schema: typeof RowDef.schema }; - Column: TRenderer & { name: "Column", schema: typeof ColumnDef.schema }; - // ... all basic components -}; - -// Angular implementation Example -// By extending ComponentDefinition, we ensure the renderer carries the required API metadata -interface AngularComponentRenderer extends ComponentDefinition { - // Angular-specific render method - render: (ctx: ComponentContext, vcr: ViewContainerRef) => any; -} - -export function createAngularBasicCatalog( - implementations: BasicCatalogImplementation -): Catalog { - return new Catalog( - "https://a2ui.org/basic_catalog.json", - Object.values(implementations) - ); -} - -// Usage -const basicCatalog = createAngularBasicCatalog({ - // If NgButton's `name` is not exactly "Button", or if its - // `schema` doesn't match ButtonDef.schema exactly, TypeScript throws an error! - Button: NgButton, - Text: NgText, - Row: NgRow, - Column: NgColumn, - // ... -}); -``` - ---- - -## Streamlined DX: The `defineCatalog` Approach - -To provide a super streamlined, `json-render`-style API for TypeScript users, we can build abstraction helpers on top of the Binder architecture. This avoids the boilerplate of defining schemas, binders, and implementations across multiple files. - -### Conceptual Streamlined API (TypeScript Web Core) - -Using the generic `ZodBinder` capability, we can construct the API directly mapping to standard A2UI types (`ChildListSchema`, `ComponentIdSchema`). - -**1. Defining the Catalog API & Schemas:** -```typescript -export const myCatalogDef = defineCatalogApi({ - id: "my-custom-catalog", - components: { - Card: { - props: z.object({ - title: DynamicStringSchema, - description: DynamicStringSchema.optional(), - child: ComponentIdSchema // Explicit A2UI component relationship - }) - }, - Button: { - props: z.object({ - label: DynamicStringSchema, - action: ActionSchema - }) - } - } -}); -``` - -**2. Implementing the React Catalog:** -The `defineCatalogImplementation` function automatically generates the generic `Binding` under the hood based on the Zod schema provided in step 1, resolving dynamic values into static values passed directly to the render function. - -For structural links like `ChildList` or `ComponentId`, the framework adapter automatically intercepts these properties and provides framework-native rendering helpers (like `renderChild(props.child)`). - -```typescript -export const myReactCatalog = defineCatalogImplementation(myCatalogDef, { - components: { - // `props` here are fully resolved strings and callbacks! - // `renderChild` is injected by the adapter to render A2UI children. - Card: ({ props, renderChild }) => ( -
-

{props.title}

- {props.description &&

{props.description}

} - {renderChild(props.child)} -
- ), - - Button: ({ props }) => ( - - ) - }, - functions: { - submit_form: async (params, context) => { ... } - } -}); -``` - -## Summary of Changes to the Renderer Guide - -To implement these updates in `renderer_guide.md`, we will: - -1. **Introduce the Binder Layer Concept**: Detail that frameworks should not manually subscribe to `DataModel` paths. Instead, they should utilize a shared `ComponentBinder` layer that outputs a framework-agnostic reactive stream of resolved properties (`ComponentBinding`). -2. **Define Framework Adapters**: Add guidance on creating framework-specific adapters (e.g., React and Angular) that handle the lifecycle of the Binding (subscription and disposal) and map its stream to native framework reactivity paradigms. -3. **Strict Catalog Typing Strategy**: Recommend that Catalog implementations expose a generic `CatalogImplementation` mapping interface so compiler errors enforce that implementations strictly match the required schema and naming signatures. -4. **Mention Advanced DX Utilities**: Outline how TypeScript implementations can leverage schema-reflection via Zod to auto-generate Binders (`createZodBinding`), while static languages can rely on code-generation for streamlined development. \ No newline at end of file From f8c147c2024644defd744ff5963cf9e764b6978b Mon Sep 17 00:00:00 2001 From: Jacob Simionato Date: Tue, 10 Mar 2026 15:24:24 +1030 Subject: [PATCH 10/13] remove dumb --- specification/v0_9/docs/renderer_guide.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/specification/v0_9/docs/renderer_guide.md b/specification/v0_9/docs/renderer_guide.md index b71c8b72f..ed692b8f0 100644 --- a/specification/v0_9/docs/renderer_guide.md +++ b/specification/v0_9/docs/renderer_guide.md @@ -32,7 +32,7 @@ The Data Layer is responsible for receiving the wire protocol (JSON messages), p > **Note on Language & Frameworks**: While the examples in this document are provided in TypeScript for clarity, the A2UI Data Layer is intended to be implemented in any language (e.g., Java, Python, Swift, Kotlin, Rust) and remain completely independent of any specific UI framework. -It consists of three sub-components: the Processing Layer, the Dumb Models, and the Context Layer. +It consists of three sub-components: the Processing Layer, the Models, and the Context Layer. ### Prerequisites @@ -95,8 +95,8 @@ The model is designed to support high-performance rendering through granular upd * **`DataContext`**: A scoped window into the `DataModel`. Used by functions and components to resolve dependencies and mutate state. * **`ComponentContext`**: A binding object pairing a component with its data scope. -### The "Dumb" Models -These classes are designed to be "dumb containers" for data. They hold the state of the UI but contain minimal logic. They are organized hierarchically. +### The Models +These classes are designed to be "simple containers" for data. They hold the state of the UI but contain minimal logic. They are organized hierarchically. #### SurfaceGroupModel & SurfaceModel The root containers for active surfaces and their catalogs, data, and components. From 2a7443417215631fc9bfbb5238281e0ea0d163c7 Mon Sep 17 00:00:00 2001 From: Jacob Simionato Date: Tue, 10 Mar 2026 15:45:46 +1030 Subject: [PATCH 11/13] Address feedback --- specification/v0_9/docs/renderer_guide.md | 117 ++++++++++++++-------- 1 file changed, 73 insertions(+), 44 deletions(-) diff --git a/specification/v0_9/docs/renderer_guide.md b/specification/v0_9/docs/renderer_guide.md index ed692b8f0..b7eb82c45 100644 --- a/specification/v0_9/docs/renderer_guide.md +++ b/specification/v0_9/docs/renderer_guide.md @@ -12,8 +12,8 @@ In highly dynamic ecosystems like the web, the architecture is typically split a * **Core Library (`web_core`)**: Implements the Core Data Layer, Component Schemas, and a Generic Binder Layer. Because TS/JS has powerful runtime reflection, the core library can provide a generic binder that automatically handles all data binding without framework-specific code. * **Framework Library (`react_renderer`, `angular_renderer`)**: Implements the Framework-Specific Adapters and the actual view implementations (the React `Button`, `Text`, etc.). -### Static Languages (e.g., Kotlin, Swift) -In statically typed languages, runtime reflection is often limited or discouraged for performance reasons. +### Static Languages (e.g., Kotlin, Swift, Dart) +In statically typed languages (and AOT-compiled languages like Dart), runtime reflection is often limited or discouraged for performance reasons. * **Core Library (e.g., `kotlin_core`)**: Implements the Core Data Layer and Component Schemas. The core library typically provides a manually implemented **Binder Layer** for the standard Basic Catalog components. This ensures that even in static environments, basic components have a standardized, framework-agnostic reactive state definition. * **Code Generation (Future/Optional)**: While the core library starts with manual binders, it may eventually offer Code Generation (e.g., KSP, Swift Macros) to automate the creation of Binders for custom components. * **Custom Components**: In the absence of code generation, developers implementing new, ad-hoc components typically utilize a **"Binderless" Implementation** flow, which allows for direct binding to the data model without intermediate boilerplate. @@ -60,7 +60,7 @@ A2UI relies on a standard observer pattern to reactively update the UI when data To ensure consistency and portability, the Data Layer implementation relies on standard patterns rather than framework-specific libraries. #### 1. The "Add" Pattern for Composition -We strictly separate **construction** from **composition**. Parent containers do not act as factories for their children. +We strictly separate **construction** from **composition**. Parent containers do not act as factories for their children. This decoupling allows child classes to evolve their constructor signatures without breaking the parent. It also simplifies testing by allowing mock children to be injected easily. * **Pattern:** ```typescript @@ -96,7 +96,14 @@ The model is designed to support high-performance rendering through granular upd * **`ComponentContext`**: A binding object pairing a component with its data scope. ### The Models -These classes are designed to be "simple containers" for data. They hold the state of the UI but contain minimal logic. They are organized hierarchically. +These classes are designed to be "simple containers" for data. They hold the state of the UI but contain minimal logic (i.e. they do not handle side effects, layout algorithms, or framework integration). + +**Key Characteristics:** +* **Mutable**: Their properties can be updated over time. +* **Observable**: They provide mechanisms to listen for those updates. +* **Encapsulated Composition**: Parent models hold references to children, but do not construct them. + +They are organized hierarchically based on the structure of the data and component tree. #### SurfaceGroupModel & SurfaceModel The root containers for active surfaces and their catalogs, data, and components. @@ -111,37 +118,43 @@ class SurfaceGroupModel { addSurface(surface: SurfaceModel): void; deleteSurface(id: string): void; getSurface(id: string): SurfaceModel | undefined; - addLifecycleListener(l: SurfaceLifecycleListener): () => void; - addActionListener(l: ActionListener): () => void; + + readonly onSurfaceCreated: EventSource>; + readonly onSurfaceDeleted: EventSource; + readonly onAction: EventSource; } -type ActionListener = (action: any) => void | Promise; // Handler for user interactions +interface ActionEvent { + surfaceId: string; + sourceComponentId: string; + name: string; + context: Record; +} + +type ActionListener = (action: ActionEvent) => void | Promise; // Handler for user interactions class SurfaceModel { readonly id: string; ... - readonly catalog: T; // Catalog containing framework-specific renderers + readonly catalog: Catalog; // Catalog containing component implementations readonly dataModel: DataModel; // Scoped application data readonly componentsModel: SurfaceComponentsModel; // Flat component map - readonly theme: any; // Theme parameters from createSurface + readonly theme?: any; // Theme parameters (validated against catalog.theme) - addActionListener(l: ActionListener): () => void; - dispatchAction(action: any): Promise; + readonly onAction: EventSource; + dispatchAction(action: ActionEvent): Promise; } ``` #### `SurfaceComponentsModel` & `ComponentModel` Manages the raw JSON configuration of components in a flat map. ```typescript -interface ComponentsLifecycleListener { - onComponentCreated: (c: ComponentModel) => void; // Called when a component is added - onComponentDeleted?: (id: string) => void; // Called when a component is removed -} - class SurfaceComponentsModel { get(id: string): ComponentModel | undefined; addComponent(component: ComponentModel): void; - addLifecycleListener(l: ComponentsLifecycleListener): () => void; + + readonly onCreated: EventSource; + readonly onDeleted: EventSource; } class ComponentModel { @@ -151,7 +164,7 @@ class ComponentModel { get properties(): Record; // Current raw JSON configuration set properties(newProps: Record); - readonly onUpdated: EventSource; // Fires when any property changes + readonly onUpdated: EventSource; // Invoked when any property changes } ``` #### `DataModel` @@ -196,7 +209,7 @@ To ensure the Data Layer behaves identically across all platforms (e.g., TypeScr | Input Type | Target Type | Result | | :------------------------- | :---------- | :----------------------------------- | -| `String` ("true", "false") | `Boolean` | `true` or `false` (case-insensitive) | +| `String` ("true", "false") | `Boolean` | `true` or `false` (case-insensitive). Any other string maps to `false`. | | `Number` (non-zero) | `Boolean` | `true` | | `Number` (0) | `Boolean` | `false` | | `Any` | `String` | Locale-neutral string representation | @@ -208,7 +221,7 @@ To ensure the Data Layer behaves identically across all platforms (e.g., TypeScr ### The Context Layer (Transient Windows) The **Context Layer** consists of short-lived objects created on-demand during the rendering process to solve the problem of "scope" and binding resolution. -Because the Data Layer is a flat list of components and a raw data tree, it doesn't inherently know about the hierarchy or the current data scope (e.g., inside a list iteration). The Context Layer bridges this gap. +Because the Data Layer is a flat list of components and a raw data tree, it doesn't inherently know about the hierarchy or the current data scope (e.g., inside a list iteration). The Context Layer bridges this gap. The appropriate "window" is determined by the structural parent components (like a `List`) which generate specific `DataContext` scopes for their children. #### `DataContext` & `ComponentContext` @@ -264,7 +277,7 @@ When processing `updateComponents`, the processor must handle existing IDs caref #### Generating Client Capabilities and Schema Types -To dynamically generate the `a2uiClientCapabilities` payload (specifically the `inlineCatalogs` array), the renderer needs to convert its internal component schemas into valid JSON Schemas that adhere to the A2UI protocol. +To dynamically generate the `a2uiClientCapabilities` payload (specifically the `inlineCatalogs` array), the renderer needs to convert its internal component and theme schemas into valid JSON Schemas that adhere to the A2UI protocol. A2UI heavily relies on shared schema definitions (like `DynamicString`, `DataBinding`, and `Action` from `common_types.json`). However, most schema validation libraries (such as Zod) do not natively support emitting external JSON Schema `$ref` pointers out-of-the-box. @@ -289,9 +302,10 @@ class Catalog { readonly id: string; // Unique catalog URI (e.g., "https://mycompany.com/catalog.json") readonly components: ReadonlyMap; readonly functions?: ReadonlyMap; + readonly theme?: Schema; // Schema for theme parameters (e.g. Zod object) - constructor(id: string, components: T[], functions?: FunctionImplementation[]) { - // Initializes the read-only maps + constructor(id: string, components: T[], functions?: FunctionImplementation[], theme?: Schema) { + // Initializes the properties } } ``` @@ -305,7 +319,8 @@ Extensibility is a core feature of A2UI. It should be trivial to create a new ca myCustomCatalog = Catalog( id="https://mycompany.com/catalogs/custom_catalog.json", functions=basicCatalog.functions, - components=basicCatalog.components.append([MyCompanyLogoComponent()]) + components=basicCatalog.components + [MyCompanyLogoComponent()], + theme=basicCatalog.theme # Inherit theme schema ) ``` @@ -385,7 +400,7 @@ For dynamic languages, you can write a generic factory that automatically inspec // Illustrative Generic Binder Factory export function createGenericBinding(schema: Schema, context: ComponentContext): ComponentBinding { // 1. Walk the schema to find all DynamicValue properties. - // 2. Map them to `context.dataContext.subscribeDynamicValue()` + // 2. Map them to `context.dataContext.subscribeDynamicValue()` // 3. Store the returned `DataSubscription` objects. // 4. Combine all observables into a single stateful stream. // 5. Return a ComponentBinding whose `dispose()` method unsubscribes all stored subscriptions. @@ -512,21 +527,35 @@ function createReactComponent(binder, RenderComponent) { ```typescript // Pseudo-code concept for an Angular adapter @Component({ - template: ` - - ` + selector: 'app-angular-wrapper', + imports: [MatButtonModule], + template: ` + @if (props(); as props) { + + } + ` }) -class AngularWrapper implements OnDestroy, OnInit { - binding: ComponentBinding; - props$: Observable; - - ngOnInit() { - this.binding = this.binder.bind(this.context); - this.props$ = this.binding.propsStream; - } - - ngOnDestroy() { - this.binding.dispose(); // Crucial cleanup +export class AngularWrapper { + private binder = inject(BinderService); + private context = inject(ComponentContext); + + private bindingResource = resource({ + loader: async () => { + const binding = this.binder.bind(this.context); + + return { + instance: binding, + props: toSignal(binding.propsStream) // Convert Observable to Signal + }; + }, + }); + + props = computed(() => this.bindingResource.value()?.props() ?? null); + + constructor() { + inject(DestroyRef).onDestroy(() => { + this.bindingResource.value()?.instance.dispose(); + }); } } ``` @@ -645,7 +674,7 @@ Implement the framework-agnostic Data Layer (Section 1). ### 4. Framework-Specific Layer Implement the bridge between the agnostic models and the native UI (Section 3). * Define the `ComponentAdapter` API (how the core library hands off a component to the framework). -* Implement the mechanism that binds a `ComponentBinding` stream to the native UI state (e.g., a wrapper wrapper view/widget). +* Implement the mechanism that binds a `ComponentBinding` stream to the native UI state (e.g., a wrapper view/widget). * Implement the recursive `Surface` builder that takes a `surfaceId`, finds the "root" component, and recursively calls `buildChild`. * **Crucial**: Ensure the unmount/dispose lifecycle hook calls `binding.dispose()`. @@ -654,7 +683,7 @@ Do not start with the full Basic Catalog. Target the `minimal_catalog.json` firs * **Core Library**: Create definitions/binders for `Text`, `Row`, `Column`, `Button`, and `TextField`. * **Core Library**: Implement the `capitalize` function. * **Framework Library**: Implement the actual native UI widgets for these 5 components. -* Write a `createMinimalCatalog()` function that bundles these together. +* Design a mechanism (e.g., a factory function or class) to bundle these together into a Catalog. ### 6. Demo Application (Milestone) Build a self-contained application to prove the architecture works before scaling. @@ -664,12 +693,12 @@ Build a self-contained application to prove the architecture works before scalin * When an example is selected, it should pipe the messages into the `MessageProcessor` and render the surface. * **Reactivity Test**: Add a mechanism to simulate delayed `updateDataModel` messages (e.g., waiting 2 seconds before sending data) to prove that the UI progressively renders and reacts to changes. -**STOP HERE. Ask the human user for approval of the architecture and demo application before proceeding to step 7.** +**STOP HERE. Ask the user for approval of the architecture and demo application before proceeding to step 7.** ### 7. Basic Catalog Support Once the minimal architecture is proven robust: -* **Core Library**: Implement the full suite of basic functions (including the complex `formatString` parser). +* **Core Library**: Implement the full suite of basic functions. It is crucial to note that string interpolation and expression parsing should ONLY happen within the `formatString` function. Do not attempt to add global string interpolation to all strings. * **Core Library**: Create definitions/binders for the remaining Basic Catalog components. * **Framework Library**: Implement all remaining UI widgets. -* **Tests**: Look at existing reference implementations (e.g., `web_core`) to formulate and run comprehensive test cases for data coercion and function logic. +* **Tests**: Look at existing reference implementations (e.g., `web_core`) to formulate and run comprehensive unit and integration test cases for data coercion and function logic. * Update the Demo App to load samples from `specification/v0_9/json/catalogs/basic/examples/`. From 46a1504428489b69ae7f9516850ac7f459b7262b Mon Sep 17 00:00:00 2001 From: Jacob Simionato Date: Tue, 10 Mar 2026 16:05:44 +1030 Subject: [PATCH 12/13] Improve explanation of scope --- specification/v0_9/docs/renderer_guide.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specification/v0_9/docs/renderer_guide.md b/specification/v0_9/docs/renderer_guide.md index b7eb82c45..350dddde7 100644 --- a/specification/v0_9/docs/renderer_guide.md +++ b/specification/v0_9/docs/renderer_guide.md @@ -92,7 +92,7 @@ The model is designed to support high-performance rendering through granular upd * **`SurfaceComponentsModel`**: A flat collection of component configurations. * **`ComponentModel`**: A specific component's raw configuration. * **`DataModel`**: A dedicated store for application data. -* **`DataContext`**: A scoped window into the `DataModel`. Used by functions and components to resolve dependencies and mutate state. +* **`DataContext`**: A scoped window into the `DataModel`. Used by functions and components to resolve dependencies and mutate state. The window is specifically a view of the data model that has some defined base path, so that relative path references are resolved against this. This is typically just the root `/`, except for cases where we are using the "template" pattern to build a `ChildList` of Components. When this occurs, the parent component will generally call some function to build each child, specifying the same component ID but a different base path for each child so that the same template can be rendered with different data. * **`ComponentContext`**: A binding object pairing a component with its data scope. ### The Models From a17edea5743fa2c47cb8d70ef33deafd99b38476 Mon Sep 17 00:00:00 2001 From: Jacob Simionato Date: Wed, 11 Mar 2026 06:56:24 +1030 Subject: [PATCH 13/13] Respond to a few more commments --- specification/v0_9/docs/renderer_guide.md | 24 +++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/specification/v0_9/docs/renderer_guide.md b/specification/v0_9/docs/renderer_guide.md index 350dddde7..21213e6c8 100644 --- a/specification/v0_9/docs/renderer_guide.md +++ b/specification/v0_9/docs/renderer_guide.md @@ -92,18 +92,18 @@ The model is designed to support high-performance rendering through granular upd * **`SurfaceComponentsModel`**: A flat collection of component configurations. * **`ComponentModel`**: A specific component's raw configuration. * **`DataModel`**: A dedicated store for application data. -* **`DataContext`**: A scoped window into the `DataModel`. Used by functions and components to resolve dependencies and mutate state. The window is specifically a view of the data model that has some defined base path, so that relative path references are resolved against this. This is typically just the root `/`, except for cases where we are using the "template" pattern to build a `ChildList` of Components. When this occurs, the parent component will generally call some function to build each child, specifying the same component ID but a different base path for each child so that the same template can be rendered with different data. +* **`DataContext`**: An abstraction around the data model, available functions, and the base path of a Component, which allows Component implementations to fetch and subscribe to dynamic values via a simple API. Different Component instances instantiated from the same Component ID, but with different base paths (e.g. because they are different instances of a *template*) can have a different `DataContext` instance. * **`ComponentContext`**: A binding object pairing a component with its data scope. ### The Models -These classes are designed to be "simple containers" for data. They hold the state of the UI but contain minimal logic (i.e. they do not handle side effects, layout algorithms, or framework integration). +These classes are designed to be "simple containers" for data. They hold a snapshot of the A2UI state and contain logic to implement observability. They may validate changes to prevent the system entering inconsistent states. Logic to decode A2UI messages and update the model layer should be within MessageProcessor. Logic to unwrap data model and function references should be within the context layer. **Key Characteristics:** * **Mutable**: Their properties can be updated over time. * **Observable**: They provide mechanisms to listen for those updates. * **Encapsulated Composition**: Parent models hold references to children, but do not construct them. -They are organized hierarchically based on the structure of the data and component tree. +They are organized hierarchically based on the structure of the data and component tree in A2UI e.g. SurfaceGroup, Surface, Component. Within each SurfaceModel, ComponentModels are represented as a flat list, with view hierarchy construction handled in the Surface rendering logic for each UI framework. #### SurfaceGroupModel & SurfaceModel The root containers for active surfaces and their catalogs, data, and components. @@ -146,7 +146,7 @@ class SurfaceModel { } ``` #### `SurfaceComponentsModel` & `ComponentModel` -Manages the raw JSON configuration of components in a flat map. +Manages the raw JSON configuration of components in a flat map which includes one entry per component ID. This represents the raw Component data *before* ChildList templates are resolved, which can instantiate multiple instances of a single Component with the same ID. ```typescript class SurfaceComponentsModel { @@ -207,15 +207,15 @@ A change at a specific path must trigger notifications for related paths to ensu #### Type Coercion Standards To ensure the Data Layer behaves identically across all platforms (e.g., TypeScript, Swift, Kotlin), the following coercion rules MUST be followed when resolving dynamic values: -| Input Type | Target Type | Result | -| :------------------------- | :---------- | :----------------------------------- | +| Input Type | Target Type | Result | +| :------------------------- | :---------- | :---------------------------------------------------------------------- | | `String` ("true", "false") | `Boolean` | `true` or `false` (case-insensitive). Any other string maps to `false`. | -| `Number` (non-zero) | `Boolean` | `true` | -| `Number` (0) | `Boolean` | `false` | -| `Any` | `String` | Locale-neutral string representation | -| `null` / `undefined` | `String` | `""` (empty string) | -| `null` / `undefined` | `Number` | `0` | -| `String` (numeric) | `Number` | Parsed numeric value or `0` | +| `Number` (non-zero) | `Boolean` | `true` | +| `Number` (0) | `Boolean` | `false` | +| `Any` | `String` | Locale-neutral string representation | +| `null` / `undefined` | `String` | `""` (empty string) | +| `null` / `undefined` | `Number` | `0` | +| `String` (numeric) | `Number` | Parsed numeric value or `0` | ### The Context Layer (Transient Windows)