From 76beacc564c6a962cd7a7c53572b835da2ff6951 Mon Sep 17 00:00:00 2001 From: Greg Spencer Date: Tue, 23 Sep 2025 13:48:57 -0700 Subject: [PATCH 1/2] Add GULF Protocol Doc. (#330) --- .../docs/genui_streaming_protocol.md | 674 ++++++++++ .../spikes/gulf_client/docs/gulf_protocol.md | 1146 +++++++++++++++++ 2 files changed, 1820 insertions(+) create mode 100644 packages/spikes/gulf_client/docs/genui_streaming_protocol.md create mode 100644 packages/spikes/gulf_client/docs/gulf_protocol.md diff --git a/packages/spikes/gulf_client/docs/genui_streaming_protocol.md b/packages/spikes/gulf_client/docs/genui_streaming_protocol.md new file mode 100644 index 000000000..c8492f2a4 --- /dev/null +++ b/packages/spikes/gulf_client/docs/genui_streaming_protocol.md @@ -0,0 +1,674 @@ +# **GenUI Streaming Protocol** + +A Specification for a JSONL-Based, Streaming UI Protocol designed for AI generation +Author: [Greg Spencer](mailto:gspencer@google.com) +Created: Sep 4, 2025 +Updated: Sep 11, 2025 + +## **Introduction** + +The GenUI Streaming Protocol (GSP) is a protocol designed for rendering user interfaces from a stream of JSON objects generated by an AI. Its core philosophy emphasizes strict decoupling of four key elements: the Catalog (client-defined contract of capabilities), the Layout (server-provided UI structure), the State (dynamic data populating the layout), and the Data Model (formal definitions for complex data objects). Communication occurs via a JSONL stream, enabling progressive rendering as the client processes each message type, including `StreamHeader`, `Layout`, `LayoutRoot`, and `StateUpdate`. The protocol also defines a clear data flow model from client initialization to server-side logic and streamed UI updates, as well as mechanisms for event handling and optional delta updates for applications which generate complex user interfaces. + +## **Section 1: Foundational Architecture and Data Flow** + +This document specifies the architecture and data formats for the "GenUI Streaming Protocol" (GSP), a protocol for rendering user interfaces from a stream of JSON objects. The design is guided by principles of strict separation of concerns, versioning, and progressive rendering, with schemas constrained for compatibility with structured data generation models. + +### **1.1. Core Philosophy: Decoupling and Contracts** + +The central philosophy of GSP is the strict decoupling of four key elements, which together define the complete user interface: + +1. **The Catalog (The Contract):** A client-defined document that specifies precisely which widgets, properties, events, and data structures the application is capable of handling. This is a static contract that governs all communication. +2. **The Layout (The Structure):** A server-provided JSON structure that describes the arrangement of widgets, their static properties, and their relationships to one another. +3. **The State (The Data):** A server-provided JSON object containing the dynamic values that populate the layout, such as text content, boolean flags, or lists of data. +4. **The Data Model (The Schemas):** A set of formal definitions for complex data objects used within the state, included within the Catalog to ensure type safety. + +### **1.2. The JSONL Stream: The Unit of Communication** + +All UI descriptions are transmitted from the server to the client as a stream of JSON objects, formatted as JSON Lines (JSONL). Each line is a separate, compact JSON object representing a single message. This allows the client to parse and process each part of the UI definition as it arrives, enabling progressive rendering. + +The stream consists of the following message types: + +- `StreamHeader`: Contains metadata about the stream, such as the protocol version and the complete initial state. This is always the first message. +- `Layout`: Contains a batch of `LayoutNode` objects. This is the primary way layouts are transmitted. A single message isn’t required to contain an entire layout, so UIs can be built incrementally. +- `LayoutRoot`: Specifies the ID of the root `LayoutNode`. The client can begin rendering once it receives this message and the corresponding `LayoutNode`. +- `StateUpdate`: Contains a partial update to the state object, used for changes after the initial render. + +### **1.3. Data Flow Model** + +The data flow is a well-defined sequence: + +1. **Client Initialization:** The app initializes its widget registry and generates its `WidgetCatalog`. +2. **Initial UI Request:** The client sends a `ClientRequest` message to the server, containing only the catalog information. +3. **Server Response & Negotiation:** The server validates the catalog. If recognized, it begins sending the UI as a JSONL stream. If not, it sends an `UnknownCatalogError` and the client requests again with the full catalog. +4. **Client-Side Progressive Rendering:** The client parses the JSONL stream and progressively renders the UI. +5. **User Interaction:** A user interacts with a widget. +6. **Event Transmission:** The client sends a new `ClientRequest` message to the server. This message includes the catalog info, the current `layout` and `state` of the UI, and an `event` object describing the user's action. +7. **Server-Side Logic & Streamed Update:** The server processes the event using the provided context and responds with a new JSONL stream containing the updated UI. + +```mermaid +sequenceDiagram + participant User + participant Client + participant Server + + rect rgba(128, 128, 128, 0.12) + note over User, Server: Initial UI Render + User->>+Client: Launches App + Client->>Client: 1. Initializes WidgetRegistry & Generates Catalog + + alt Catalog is known by server + Client->>+Server: 2. Sends ClientRequest (CatalogReference and Supplemental Catalog) + Server-->>-Client: 3. Responds with JSONL Stream + else Catalog is unknown by server + Client->>+Server: 2. Sends ClientRequest (CatalogReference and Supplemental Catalog) + Server-->>-Client: 3a. Responds with "Unknown Catalog" Error + Client->>+Server: 3b. Re-sends ClientRequest (Full Catalog) + Server-->>-Client: 3c. Responds with JSONL Stream + end + Note right of Client: 4. Parses stream and progressively
renders UI as nodes arrive. + Client-->>-User: Displays UI as it builds + end + + loop Interaction & Update Cycle + User->>+Client: 5. Interacts with a rendered widget (e.g., tap) + Client->>+Server: 6. Sends ClientRequest (with Event, Layout, and State) + Note left of Server: 7. Processes event and prepares new UI. + Server-->>-Client: 8. Responds with a new JSONL stream + Note right of Client: 9. Replaces UI with new progressively
rendered view. + Client-->>-User: Displays Updated UI + end +``` + +## **Section 2: The Widget Catalog: Defining Capabilities and Data Models** + +The `WidgetCatalog` is a JSON document that serves as a strict contract of the client's rendering and data-handling capabilities. While it can be a static file bundled with the application, it is typically generated at runtime from a `WidgetCatalogRegistry` where widget builders and their definitions are registered in code. + +### **2.1. Purpose: The Client-Server Contract** + +The catalog explicitly declares the client's capabilities, enabling: + +- **Server-Side Validation:** The server can validate any UI definitions it receives from the AI against the client's catalog before sending it. +- **Versioning and Coexistence:** The server can support a set of known catalog versions, allowing it to generate compatible UI for different client versions without requiring the client to send its full capabilities on every request. +- **Guided AI Generation:** The catalog provides a structured schema that can be used to constrain the output of a Large Language Model, ensuring it only generates valid, renderable UI definitions. +- **Formalized Data Structures:** It allows for defining complex data types, ensuring that `state` objects are well-formed and type-safe. + +### **2.2. Catalog Schema (`WidgetCatalog`)** + +The catalog is a top-level JSON object: + +- `catalogVersion`: A string representing the version of the catalog file itself (e.g., "2.1.0"). +- `dataTypes`: An object where each key is a custom data type name (e.g., `User`), and the value is a JSON Schema definition for that type. +- `items`: An object where each key is a widget type name (e.g., `Container`), and the value is a `WidgetDefinition` object. + +### **2.3. Custom Data Type Definitions (`dataTypes`)** + +This section allows the client to declare reusable, complex data structures. This is crucial for domain modeling and ensuring that state updates are type-safe. + +The value of each key under `dataTypes` must be a valid JSON Schema object. + +```json +// Example: A dataTypes block defining a nested TodoItem model +"dataTypes": { + "TodoItem": { + "type": "object", + "properties": { + "id": { "type": "string" }, + "details": { + "type": "object", + "properties": { + "text": { "type": "string" }, + "priority": { "type": "string", "enum": ["low", "medium", "high"] } + }, + "required": ["text"] + }, + "isCompleted": { "type": "boolean", "default": false } + }, + "required": ["id", "details"] + } +} +``` + +### **2.4. Widget Definition Schema (`WidgetDefinition`)** + +This object describes a single renderable widget type. It uses the JSON Schema standard to define its properties. + +- `properties`: A **JSON Schema object** that defines the supported attributes for the widget. This allows for rich validation, including types, patterns, required fields, and nested objects. +- `events`: An object defining the events this widget can emit. Each key is an event name (e.g., `onPressed`), and the value is a JSON Schema defining the structure of the `arguments` object for that event. + +```json +// Example: A WidgetDefinition for a 'ListItem' widget +"ListItem": { + "properties": { + "type": "object", + "properties": { + "text": { "type": "string" }, + "isCompleted": { "type": "boolean", "default": false } + }, + "required": ["text"] + }, + "events": { + "onToggled": { + "type": "object", + "properties": { + "newState": { "type": "boolean" } + } + } + } +} +``` + +## **Section 3: UI Composition: The Streaming Layout** + +To meet the constraint of a non-recursive format and enable streaming, the GSP layout is defined by a stream of `LayoutNode` messages. + +### **3.1. The Adjacency List Model** + +The layout is composed of `LayoutNode` objects that arrive individually over the stream. Parent-child relationships are established through ID references. This model allows for the client to buffer nodes as they arrive and assemble the UI tree once the root is known, and to continue to build it out as more nodes are received. + +If the server sends a container with ids that aren't yet defined, the client can skip rendering those children until they are defined. + +The following diagram illustrates how the client processes the stream to build the final UI: + +```mermaid +flowchart TD + subgraph "Server Stream (JSONL)" + A("Layout
nodes: [nodeA, nodeB, nodeC]") + B("LayoutRoot
rootId: 'nodeA'") + end + + subgraph "Client-Side Buffer" + C("nodeA:
{type: Column,
properties: {
children: ['nodeB', 'nodeC'] } }") + D("nodeB:
{type: Text,
properties: {
$text: '/welcomeMessage' } }") + E("nodeC:
{type: Image,
properties: {
assetName: 'logo.png' } }") + end + + subgraph "Rendered Widget Tree" + F(Column) --> G(Text) + F --> H(Image) + end + + A -- "Parsed and stored" --> C + A -- "Parsed and stored" --> D + A -- "Parsed and stored" --> E + + B -- "Triggers build from buffer" --> F +``` + +### **3.2. Layout Node Schema (`LayoutNode`)** + +A `LayoutNode` message is a JSON object with the following keys: + +- `id`: A required, unique string that identifies this specific widget instance. +- `type`: A string that must match a widget type name in the `WidgetCatalog`. +- `properties`: An object containing the properties for this widget. A property's value can be a static literal (string, number, boolean, etc.) or a binding object. +- `itemTemplate`: For list-building widgets, this is a complete `LayoutNode` object used as a template. See Section 3.3. + +A property is considered a dynamic binding if its value is a JSON object containing a `"$bind"` key. The value of `"$bind"` is the path to the data in the state object. Any other keys in the object are transformations. + +### **3.4. Advanced Composition: List Rendering with Builders** + +To efficiently render dynamic lists (e.g., search results), the protocol supports a builder pattern. This avoids defining a `LayoutNode` for every single item in a list. + +A special widget type, e.g., `ListViewBuilder`, can be defined in the catalog. It uses a combination of data binding and a template to generate its children. + +- **`properties`**: The builder widget itself has properties. Static ones have literal values, while dynamic ones use a binding object. + +- **`itemTemplate`**: It contains a single `LayoutNode` definition that serves as a template for each item. This template can use relative binding paths, which the client resolves for each element of the bound `data` list. + +```json +// Example: A node for a ListViewBuilder using the $bind property syntax +{ + "id": "todo_list", + "type": "ListViewBuilder", + "properties": { + "scrollDirection": "vertical", + "items": { "$bind": "/todoItems" } + }, + "itemTemplate": { + "id": "todo_item_template", + "type": "ListItem", + "properties": { + "text": { "$bind": "details/text" }, + "isCompleted": { "$bind": "isCompleted" } + } + } +} +``` + +## **Section 4: Dynamic Data: The State Management Schema** + +GSP enforces a clean separation between the UI's structure (layout) and its dynamic data (state). + +### **4.1. The `state` Object: A Centralized Data Store** + +The `initialState` object in the `StreamHeader` message provides the initial, complete state for the UI. Subsequent changes are delivered by `StateUpdate` messages. This state object is the sole source of truth for all dynamic data. By defining the structure of its contents in the catalog's `dataTypes` section, the state object is not an arbitrary blob but a well-defined, validatable data model. This model can be returned as part of UI events to tell the server what change is represented by the event, or sent from the server to update the dynamic state of the UI. + +### **4.2. Data Binding with Transformations** + +The `properties` map within a `LayoutNode` forges the connection between layout and state. A property is bound to state if its value is an object containing a `"$bind"` key. + +```json +// General structure of a binding object +"properties": { + "widgetProperty": { + "$bind": "/path/to/data", // Path to data in the state object + // Optional transformers below + "format": "Value is: {}" + } +} +``` + +**Supported Transformations:** + +- **`format`**: A string with a `{}` placeholder, which will be replaced by a string representation of the value from the bound path. + + - Example: `{"$bind": "text", "format": "Todo: {}"}` (Relative path) + +- **`condition`**: Evaluates a boolean value from the bound path. + + - `ifValue`: The value to use if the path is `true`. + + - `elseValue`: The value to use if the path is `false`. + + - Example: `{"$bind": "isCompleted", "condition": {"ifValue": "Done!", "elseValue": "Pending"}}` (Relative path) + +- **`map`**: Maps a value to another value. + + - `mapping`: An object where keys are possible state values and values are the desired output. + + - `fallback`: A value to use if the state value is not in the map. + + - Example: `{"$bind": "/appStatus", "map": {"mapping": {"active": "#FF00FF00", "inactive": "#FFFF0000"}, "fallback": "#FF888888"}}` (Absolute path) + +This small, predefined set of transformers adds significant declarative power without the security risks of a full expression language. + +In the future, other transformations could be added, such as defining a query, and then have that expand to the results of the query against a database. + +## **Section 5: Event Handling** + +### **5.1. Client-to-Server: The `ClientRequest` Message** + +All communication from the client to the server is handled by a single, unified `ClientRequest` message. The presence of the optional `event` field determines the purpose of the request. + +- **Initial UI Request:** The client sends a `ClientRequest` with only the `catalogReference` and/or `catalog` fields. +- **User Interaction:** The client sends a `ClientRequest` that includes the catalog information, the current `layout` and `state` of the UI, and an `event` object describing the interaction. + +The `ClientRequest` is structured as follows: + +- `catalogReference`: An optional object identifying a predefined base catalog. +- `catalog`: An optional `WidgetCatalog` object containing the full catalog or just additions. +- `event`: An optional object describing the user interaction. Its presence indicates a user event. + - `sourceNodeId`: The `id` of the `LayoutNode` that generated the event. + - `eventName`: The name of the event (e.g., `onPressed`). + - `timestamp`: An ISO 8601 string of when the event occurred. + - `arguments`: An optional object with event-specific data. +- `layout`: The complete `Layout` object of the current UI. Sent when `event` is present. +- `state`: The complete `state` object of the current UI. Sent when `event` is present. +- `renderError`: An optional object describing a client-side rendering error. Its presence indicates that the previously supplied layout failed to render. + - `errorType`: A string identifying the type of error (e.g., `UnknownWidgetType`). + - `message`: A detailed, human-readable message about the error. + - `sourceNodeId`: The `id` of the `LayoutNode` that caused the error. + - `fullLayout`: The complete `Layout` object that failed to render. + - `currentState`: The `state` object at the time of the error. + +### **5.2. Server-to-Client: Streamed UI Update** + +After processing an event or a `renderError`, the server responds with a new JSONL stream. This stream contains the complete definition for the new UI, allowing the client to progressively render the updated view. + +## **Section 6: Complete JSON Schema Definitions** + +This section provides the formal, consolidated, and valid JSON Schema definitions for the GSP protocol. Each message in the JSONL stream must be one of the types defined in `oneOf`. + +### **6.1. GSP Stream Message Schema** + +```json +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://example.com/GSP-stream-schema-v1.json", + "title": "GSP Stream Message Schema", + "description": "A schema for a single message in a GSP JSONL stream.", + "type": "object", + "$defs": { + "StreamHeader": { + "type": "object", + "properties": { + "messageType": { "const": "StreamHeader" }, + "formatVersion": { + "type": "string", + "pattern": "^\\d+\\.\\d+\\.\\d+$" + }, + "initialState": { "type": "object" } + }, + "required": ["messageType", "formatVersion"] + }, + "BindingObject": { + "type": "object", + "properties": { + "$bind": { "type": "string" }, + "format": { "type": "string" }, + "condition": { + "type": "object", + "properties": { + "ifValue": {}, + "elseValue": {} + } + }, + "map": { + "type": "object", + "properties": { + "mapping": { "type": "object" }, + "fallback": {} + } + } + }, + "required": ["$bind"] + }, + "LayoutNode": { + "type": "object", + "properties": { + "id": { "type": "string" }, + "type": { "type": "string" }, + "properties": { + "type": "object", + "additionalProperties": { + "anyOf": [ + { "type": "string" }, + { "type": "number" }, + { "type": "boolean" }, + { "type": "object" }, + { "type": "array" }, + { "$ref": "#/$defs/BindingObject" } + ] + } + }, + "itemTemplate": { "$ref": "#/$defs/LayoutNode" } + }, + "required": ["id", "type"] + }, + "Layout": { + "type": "object", + "properties": { + "messageType": { "const": "Layout" }, + "nodes": { + "type": "array", + "items": { "$ref": "#/$defs/LayoutNode" } + } + }, + "required": ["messageType", "nodes"] + }, + "LayoutRoot": { + "type": "object", + "properties": { + "messageType": { "const": "LayoutRoot" }, + "rootId": { "type": "string" } + }, + "required": ["messageType", "rootId"] + }, + "StateUpdate": { + "type": "object", + "properties": { + "messageType": { "const": "StateUpdate" }, + "state": { "type": "object" } + }, + "required": ["messageType", "state"] + }, + "CatalogReference": { + "type": "object", + "properties": { + "name": { "type": "string" }, + "version": { "type": "string", "pattern": "^\\d+\\.\\d+\\.\\d+$" } + }, + "required": ["name", "version"] + }, + "PartialWidgetCatalog": { + "type": "object", + "properties": { + "dataTypes": { + "type": "object", + "description": "A map of custom data type names to their JSON Schema definitions.", + "additionalProperties": { "type": "object" } + }, + "items": { + "type": "object", + "additionalProperties": { "$ref": "#/$defs/WidgetDefinition" } + } + } + }, + "Event": { + "type": "object", + "properties": { + "sourceNodeId": { "type": "string" }, + "eventName": { "type": "string" }, + "timestamp": { "type": "string", "format": "date-time" }, + "arguments": { "type": "object" } + }, + "required": ["sourceNodeId", "eventName", "timestamp"] + }, + "RenderError": { + "type": "object", + "properties": { + "errorType": { "type": "string" }, + "message": { "type": "string" }, + "sourceNodeId": { "type": "string" }, + "fullLayout": { "$ref": "#/$defs/Layout" }, + "currentState": { "type": "object" } + }, + "required": [ + "errorType", + "message", + "sourceNodeId", + "fullLayout", + "currentState" + ] + }, + "ClientRequest": { + "type": "object", + "properties": { + "messageType": { "const": "ClientRequest" }, + "catalogReference": { "$ref": "#/$defs/CatalogReference" }, + "catalog": { "$ref": "#/$defs/PartialWidgetCatalog" }, + "event": { "$ref": "#/$defs/Event" }, + "renderError": { "$ref": "#/$defs/RenderError" }, + "layout": { "$ref": "#/$defs/Layout" }, + "state": { "type": "object" } + } + }, + "UnknownCatalogError": { + "type": "object", + "properties": { + "messageType": { "const": "UnknownCatalogError" }, + "error": { "const": "UnknownCatalog" }, + "message": { "type": "string" } + }, + "required": ["messageType", "error", "message"] + } + }, + "oneOf": [ + { "$ref": "#/$defs/StreamHeader" }, + { "$ref": "#/$defs/Layout" }, + { "$ref": "#/$defs/LayoutRoot" }, + { "$ref": "#/$defs/StateUpdate" }, + { "$ref": "#/$defs/ClientRequest" }, + { "$ref": "#/$defs/UnknownCatalogError" } + ] +} +``` + +## **Section 7: Client-Side Implementation and Best Practices** + +### **7.1. The GSP Interpreter** + +A robust client-side interpreter for a streaming protocol should be composed of several key components: + +- **JSONL Parser:** A parser capable of reading the stream and decoding each line as a separate JSON object. +- **Message Dispatcher:** A mechanism to identify the `messageType` of each incoming JSON object and dispatch it to the appropriate handler (e.g., `StreamHeader`, `Layout`, `LayoutRoot`). +- **Layout Node Buffer:** A temporary storage (e.g., a `Map`) for `LayoutNode` objects received in `Layout` messages. +- **Widget Tree Builder:** Constructs the widget tree. Once the `LayoutRoot` message arrives, the builder can start assembling the tree using the nodes in the buffer. +- **State Manager:** Holds the state object, initialized from the `StreamHeader`'s `initialState`, and applies subsequent `StateUpdate` messages. +- **Binding Processor:** Resolves bindings by inspecting the `properties` of a `LayoutNode`. If a property's value is an object with a `"$bind"` key, it's treated as a dynamic binding. Otherwise, it's a static value. + +### **7.2. Progressive Rendering** + +The primary advantage of the streaming approach is the ability to render the UI as it arrives: + +1. The client begins parsing the JSONL stream. +2. As `LayoutNode` messages are received, they are stored in the buffer. +3. When the `LayoutRoot` message arrives, the client can immediately start building the widget tree, starting from the specified root ID. +4. If the root node is already in the buffer, it is rendered. If not, the client waits for it to arrive. +5. As more `LayoutNode` messages arrive, the client can fill in the missing parts of the widget tree. The UI will appear to "build itself" on the screen. + +### **7.3. Performance Considerations** + +- **Efficient List Building:** Use the underlying UI framework's mechanisms for efficient list building (e.g., `ListView.builder` in Flutter) to ensure that list items are only built as they are scrolled into view. +- **`const` Widgets:** Use `const` widgets in the client-side mapping code wherever possible. + +### **7.4. Error Handling** + +- **Invalid Payloads:** Handle malformed JSON or messages that do not conform to the schema. +- **Catalog Violations:** If the server sends a layout that violates the `WidgetCatalog`, the client should log the error and display a fallback UI. +- **Broken Bindings:** If a binding path is invalid, the client should use a default value or display a visual error indicator. + +## **Appendix A: Delta Updates** + +For performance-critical applications where sending the entire UI definition on every interaction is too costly, GSP supports an optional delta update mechanism. This allows the server to send only the changes to the layout or state. + +### **A.1. Server-to-Client: The `StateUpdate` Payload** + +To change only dynamic data, the server can send a `StateUpdate` payload. This payload uses a simplified model that is easy for AI to generate and avoids the complexity of index- or key-based list modifications. + +The payload contains a single `operations` array, where each object is a specific command. + +#### **A.1.1. The `stateSet` Operation** + +This operation replaces the value at a given absolute path. If the path points to a list, the entire list is replaced. This is the primary mechanism for updating state. + +- `op`: `"stateSet"` +- `path`: An absolute JSON Pointer-like path to a value in the state (e.g., `/todoItems`, `/user/name`). +- `value`: The new value to set at the specified path. + +```json +{ + "op": "stateSet", + "path": "/todoItems", + "value": [ + { "id": "todo-123", "text": "Buy almond milk", "isCompleted": true }, + { "id": "todo-456", "text": "Call the bank", "isCompleted": false } + ] +} +``` + +#### **A.1.2. The `listAppend` Operation** + +This operation provides a convenient shortcut for appending one or more items to the end of a list without needing to send the entire list. + +- `op`: `"listAppend"` +- `path`: An absolute path to the target list. +- `items`: An array of full item objects to add. + +```json +{ + "op": "listAppend", + "path": "/todoItems", + "items": [ + { "id": "todo-789", "text": "Schedule appointment", "isCompleted": false } + ] +} +``` + +### **A.2. Server-to-Client: The `LayoutUpdate` Payload** + +For surgical modifications to the UI's structure, the server sends a `LayoutUpdate` payload with a simpler, custom operation set. + +- `operations`: An array of layout modification objects. Each object specifies an `op` (`add`, `remove`, `replace`), the nodes to modify, and targeting information. + +### **A.3. Delta Update Schemas** + +```json +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://example.com/GSP-delta-payloads-schema-v2.json", + "title": "GSP Delta Update Payloads Schema", + "description": "A collection of schemas for delta updates in the GSP protocol.", + "type": "object", + "$defs": { + "StateUpdate": { + "type": "object", + "properties": { + "operations": { + "type": "array", + "description": "A series of operations to apply to the state.", + "items": { + "oneOf": [ + { + "type": "object", + "title": "State Set Operation", + "properties": { + "op": { "const": "stateSet" }, + "path": { "type": "string" }, + "value": {} + }, + "required": ["op", "path", "value"] + }, + { + "type": "object", + "title": "List Append Operation", + "properties": { + "op": { "const": "listAppend" }, + "path": { "type": "string" }, + "items": { "type": "array" } + }, + "required": ["op", "path", "items"] + } + ] + } + } + }, + "required": ["operations"] + }, + "LayoutUpdate": { + "type": "object", + "properties": { + "operations": { + "type": "array", + "items": { + "type": "object", + "properties": { + "op": { + "type": "string", + "enum": ["add", "remove", "replace"] + }, + "nodes": { + "type": "array", + "items": { + "$ref": "#/$defs/LayoutNode" + } + }, + "nodeIds": { + "type": "array", + "items": { + "type": "string" + } + }, + "targetNodeId": { + "type": "string" + }, + "targetProperty": { + "type": "string" + } + }, + "required": ["op"] + } + } + }, + "required": ["operations"] + } + } +} +``` + +### **A.4. Client-Side Implementation for Delta Updates** + +When implementing client-side support for delta updates, the following components are necessary: + +- **Update Applier:** A component that processes incoming `StateUpdate` and `LayoutUpdate` payloads, applying the specified operations to the client's internal representation of the state and layout. +- **Granular Rebuilds:** The client should be able to trigger a rebuild of only the specific widgets affected by a state change, rather than rebuilding the entire tree. This is a key performance benefit of the delta update mechanism. diff --git a/packages/spikes/gulf_client/docs/gulf_protocol.md b/packages/spikes/gulf_client/docs/gulf_protocol.md new file mode 100644 index 000000000..0387c5dde --- /dev/null +++ b/packages/spikes/gulf_client/docs/gulf_protocol.md @@ -0,0 +1,1146 @@ +# GULF (Generative UI Language Format) Protocol + +A Specification for a JSONL-Based, Streaming UI Protocol + +Created: Sep 19, 2025 + +## Design Requirements + +The GULF protocol should be a system where an LLM can stream a platform-agnostic, abstract UI definition to a client, which then renders it progressively using a native widget set. Every major design choice is traced back to the core challenges of LLM generation, perceived performance, and platform independence. + +### Requirement: The protocol must be easily generated by a Transformer Large Language Model (LLM) + +This is the most critical driver. "LLM-friendliness" is explicitly mentioned. This requirement directly leads to several design choices: + +Declarative, Simple Structure: The protocol should use a straightforward, declarative format ("this is a column with these children") rather than an imperative one ("now, add a column; then, append a text widget to it"). LLMs excel at generating structured, declarative data. + +Flat Component List (Adjacency List): Requiring an LLM to generate a perfectly nested JSON tree in a single pass is difficult and error-prone. A flat list of components, where relationships are defined by simple string IDs, is much easier to generate piece by piece. The model can "think" of a component, give it an ID, and then reference that ID later without worrying about tree depth or object nesting. + +Stateless Messages: Each JSONL message is a self-contained unit of information (componentUpdate, dataModelUpdate). This is ideal for a streaming LLM, which can output these messages incrementally as it processes a request. + +### Requirement: The UI must render progressively for a fast, responsive user experience + +The system must feel fast to the user, even if the full UI is complex and takes time to generate. + +Streaming via JSONL/SSE: This is a direct solution. The client doesn't have to wait for a single, massive JSON payload. It can start receiving and processing UI components immediately, improving perceived performance. + +### Requirement: The protocol must be platform-agnostic + +The same server-side logic should be able to render a UI on a Flutter app, a web browser, or potentially other platforms without modification. + +Client-Defined widget catalog: This is the core of the platform-agnostic design. The protocol should define an abstract component tree (e.g., "I need a Card with a Row inside"). It is the client's responsibility to map these abstract types to its native widget implementations (a Flutter Card widget, an HTML `
` with card styling, etc.). The server only needs to know the names of the components the client supports. + +### Requirement: State management must be efficient and decoupled from the UI structure + +Changing a piece of text in the UI should not require resending the entire UI definition. + +Separation of Data and Components: Having distinct componentUpdate and data model update messages is key. The UI structure can be sent once, and subsequent updates can be small dataModelUpdate messages that only contain the changed data. + +### Requirement: The communication architecture must be robust and scalable + +The system needs a clear, reliable way to handle both server-pushed UI and client-initiated events. + +Unidirectional UI Stream: Using a one-way stream (SSE) for UI updates simplifies the client's logic. It only needs to listen and react. This is a more robust pattern for server-push than trying to manage a complex bidirectional channel. + +Out-of-Band Event Handling (REST API): Using a standard, stateless REST endpoint for user actions leverages a well-understood, highly scalable, and reliable web technology. It cleanly separates the concerns of UI rendering from event processing. + +## Introduction + +The GULF (Generative UI Language Format) Protocol is a protocol designed for rendering user interfaces from a stream of JSON objects sent from a server. Its core philosophy emphasizes a clean separation of UI structure and application data, enabling progressive rendering as the client processes each message. + +The protocol is designed to be "LLM-friendly," meaning its structure is declarative and straightforward, making it easy for a generative model to produce. + +Communication occurs via a JSON Lines (JSONL) stream. The client parses each line as a distinct message and incrementally builds the UI. The server-to-client protocol defines four message types: + +- `streamHeader`: Identifies the protocol version. This is always the first message. +- `componentUpdate`: Provides a list of component definitions to be added to the client's component buffer. +- `dataModelUpdate`: Provides new data to be inserted into or to replace the client's data model. +- `beginRendering`: Signals to the client that it has enough information to perform the initial render, specifying the ID of the root component. + +Client-to-server communication for user interactions is handled separately via a `ClientEvent` message, sent as a JSON payload to a REST API. This keeps the primary data stream unidirectional. + +## Section 1: Foundational Architecture and Data Flow + +This document specifies the architecture and data formats for the GULF protocol. The design is guided by principles of strict separation of concerns, versioning, and progressive rendering. + +### 1.1. Core Philosophy: Decoupling and Contracts + +The central philosophy of GULF is the decoupling of three key elements: + +1. **The Component Tree (The Structure):** A server-provided tree of abstract components that describes the UI's structure. This is defined by `componentUpdate` messages. +2. **The Data Model (The State):** A server-provided JSON object containing the dynamic values that populate the UI, such as text, booleans, or lists. This is managed via `dataModelUpdate` messages. +3. **The Widget Registry (The "Catalog"):** A client-defined mapping of component types (e.g., "Row", "Text") to concrete, native widget implementations. This registry is **part of the client application**, not the protocol stream. The server must generate components that the target client's registry understands. + +### 1.2. The JSONL Stream: The Unit of Communication + +All UI descriptions are transmitted from the server to the client as a stream of JSON objects, formatted as JSON Lines (JSONL). Each line is a separate, compact JSON object representing a single message. This allows the client to parse and process each part of the UI definition as it arrives, enabling progressive rendering. + +### 1.3. Data Flow Model + +The GULF protocol is composed of a server-to-client stream describing UI and individual events sent to the server. The client consumes the stream, builds the UI, and renders it. Communication occurs via a JSON Lines (JSONL) stream, typically transported over **Server-Sent Events (SSE)**. + +1. **Server Stream:** The server begins sending the JSONL stream over an SSE connection. +2. **Client-Side Buffering:** The client receives messages and buffers them: + + - `streamHeader`: The client validates the version. + - `componentUpdate`: Component definitions are stored in a `Map`. + - `dataModelUpdate`: The client's internal JSON data model is built or updated. + +3. **Render Signal:** The server sends a `beginRendering` message with the `root` component's ID. This prevents a "flash of incomplete content." The client buffers incoming components and data but waits for this explicit signal before attempting the first render, ensuring the initial view is coherent. +4. **Client-Side Rendering:** The client, now in a "ready" state, starts at the `root` component. It recursively walks the component tree by looking up component IDs in its buffer. It resolves any data bindings against the data model and uses its `WidgetRegistry` to instantiate native widgets. +5. **User Interaction and Event Handling:** The user interacts with a rendered widget (e.g., taps a button). The client constructs a `ClientEvent` JSON payload, resolving any data bindings from the component's `action.context`. It sends this payload to a pre-configured REST API endpoint on the server via a `POST` request. +6. **Dynamic Updates:** The server processes the `ClientEvent`. If the UI needs to change in response, the server sends new `componentUpdate` and `dataModelUpdate` messages over the original SSE stream. As these arrive, the client updates its component buffer and data model, and the UI re-renders to reflect the changes. + +```mermaid +sequenceDiagram + participant Server + participant Client + + Server->>+Client: SSE Connection (JSONL Stream) + Client->>Client: 1. Parse JSONL message + loop Until 'beginRendering' + Client->>Client: 2a. Process streamHeader (check version) + Client->>Client: 2b. Process componentUpdate (store in component map) + Client->>Client: 2c. Process dataModelUpdate (update data model) + end + Client->>Client: 3. Process beginRendering (rootId: 'root', isReady: true) + Note right of Client: 4. Triggers UI build + Client->>Client: 5. Build widget tree from 'root' + Client->>Client: 6. Resolve data bindings + Client->>Client: 7. Look up widgets in WidgetRegistry + Client-->>-Server: (UI is rendered) + + Note over Client: 8. User interacts with UI (e.g., clicks button) + Client->>Client: 9. Construct ClientEvent payload + Client->>+Server: 10. POST /event (ClientEvent JSON) + Server-->>-Client: 11. HTTP 200 OK + + loop Dynamic Updates in Response to Event + Server->>+Client: componentUpdate or dataModelUpdate (via SSE) + Client->>Client: Update component map or data model + Note right of Client: Triggers UI rebuild + Client-->>-Server: (UI is updated) + end +``` + +### 1.4. Full Stream Example + +The following is a complete, minimal example of a JSONL stream that renders a user profile card. + +```jsonl +{"streamHeader": {"version": "1.0.0"}} +{"componentUpdate": {"components": [{"id": "root", "componentProperties": {"Column": {"children": {"explicitList": ["profile_card"]}}}}]}} +{"componentUpdate": {"components": [{"id": "profile_card", "componentProperties": {"Card": {"child": "card_content"}}}]}} +{"componentUpdate": {"components": [{"id": "card_content", "componentProperties": {"Column": {"children": {"explicitList": ["header_row", "bio_text"]}}}}]}} +{"componentUpdate": {"components": [{"id": "header_row", "componentProperties": {"Row": {"alignment": "center", "children": {"explicitList": ["avatar", "name_column"]}}}}]}} +{"componentUpdate": {"components": [{"id": "avatar", "componentProperties": {"Image": {"url": {"literalString": "[https://www.example.com/profile.jpg)"}}}}]}} +{"componentUpdate": {"components": [{"id": "name_column", "componentProperties": {"Column": {"alignment": "start", "children": {"explicitList": ["name_text", "handle_text"]}}}}]}} +{"componentUpdate": {"components": [{"id": "name_text", "componentProperties": {"Heading": {"level": "3", "text": {"literalString": "Flutter Fan"}}}}]}} +{"componentUpdate": {"components": [{"id": "handle_text", "componentProperties": {"Text": {"text": {"literalString": "@flutterdev"}}}}]}} +{"componentUpdate": {"components": [{"id": "bio_text", "componentProperties": {"Text": {"text": {"literalString": "Building beautiful apps from a single codebase."}}}}]}} +{"dataModelUpdate": {"contents": {}}} +{"beginRendering": {"root": "root"}} +``` + +## Section 2: The Component Model + +Unlike protocols with an explicit catalog, GULF relies on a **client-defined** `WidgetRegistry`. The set of available component types (e.g., `Row`, `Text`, `Card`) and their supported properties is defined by the native client application. The server must generate `componentUpdate` messages that conform to the component set expected by the client. + +### 2.1. The `componentUpdate` Message + +This message is the primary way UI structure is defined. It contains a `components` array, which is a flat list of component instances. + +```json +{ + "componentUpdate": { + "components": [ + { + "id": "unique-component-id", + "weight": 1.0, + "componentProperties": { + "Text": { + "text": { "literalString": "Hello, World!" } + } + } + }, + { + "id": "another-component-id", + "componentProperties": { ... } + } + ] + } +} +``` + +### 2.2. The Component Object + +Each object in the `components` array has the following structure: + +- `id`: A required, unique string that identifies this specific component instance. This is used for parent-child references. +- `weight`: An optional number used by `Row` and `Column` containers to determine proportional sizing. +- `componentProperties`: A required object that defines the component's type and properties. + +### 2.3.`componentProperties` (Discriminated Union) + +This object uses a "discriminated union" pattern. It **must** contain exactly one key, where the key is the string name of the component type (e.g., `"Text"`, `"Row"`, `"Button"`). The value of that key is an object containing the specific properties for that component. + +**Example:** A `Text` component: + +```json +"componentProperties": { + "Text": { + "text": { "literalString": "This is text" } + } +} +``` + +A `Button` component: + +```json +"componentProperties": { + "Button": { + "label": { "literalString": "Click Me" }, + "action": { "action": "submit_form" } + } +} +``` + +The full set of available component types and their properties is defined by the `gulf_schema.json`. + +## Section 3: UI Composition + +### 3.1. The Adjacency List Model + +The GULF protocol defines the UI as a flat list of components. The tree structure is built implicitly using ID references. This is known as an adjacency list model. + +Container components (like `Row`, `Column`, `List`, `Card`) have properties that reference the `id` of their child component(s). The client is responsible for storing all components in a map (e.g., `Map`) and recreating the tree structure at render time. + +This model allows the server to send component definitions in any order, as long as all necessary components are present by the time `beginRendering` is sent. + +```mermaid +flowchart TD + subgraph "Server Stream (JSONL)" + A("componentUpdate
components: [root, title, button]") + B("beginRendering
root: 'root'") + end + + subgraph "Client-Side Buffer (Map)" + C("root: {id: 'root', type: Column, children: ['title', 'button']}") + D("title: {id: 'title', type: Text, text: 'Welcome'}") + E("button: {id: 'button', type: Button, label: 'Go'}") + end + + subgraph "Rendered Widget Tree" + F(Column) --> G(Text: 'Welcome') + F --> H(Button: 'Go') + end + + A -- "Parsed and stored" --> C + A -- "Parsed and stored" --> D + A -- "Parsed and stored" --> E + + B -- "Triggers build from buffer" --> F +``` + +### 3.2. Container Children: `explicitList` vs. `template` + +Container components (`Row`, `Column`, `List`) define their children using a `children` object, which must contain _either_ `explicitList` or `template`. + +- `explicitList`: An array of component `id` strings. This is used for static, known children. +- `template`: An object used to render a dynamic list of children from a data-bound list. + +```json +// Example: Children property from gulf_schema.json +"children": { + "description": "Defines the children... You MUST define EITHER 'explicitList' OR 'template'", + "properties": { + "explicitList": { + "type": "array", + "items": { "type": "string" } + }, + "template": { + "type": "object", + "properties": { + "componentId": { "type": "string" }, + "dataBinding": { "type": "string" } + }, + "required": ["componentId", "dataBinding"] + } + } +} +``` + +### 3.3. Dynamic List Rendering with `template` + +To render dynamic lists, a container uses the `template` property. + +1. `dataBinding`: A path to a list in the data model (e.g., `user.posts`). +2. `componentId`: The `id` of another component in the buffer to use as a template for each item in the list. + +The client will iterate over the list at `dataBinding` and, for each item, render the component specified by `componentId`. The item's data is made available to the template component for relative data binding. + +## Section 4: Dynamic Data & State Management + +GULF enforces a clean separation between the UI's structure (components) and its dynamic data (data model). + +### 4.1. The `dataModelUpdate` Message + +This message is the only way to modify the client's data model. + +- `contents`: The JSON content to be inserted. +- `path`: An optional, dot-separated path string. + + - If `path` is `null` or empty, the `contents` will **completely replace** the entire data model. + - If `path` is provided (e.g., `user.name` or `user.addresses[0].street`), the client will traverse the data model and insert the `contents` at that specific location, creating nested objects/lists as needed. + +#### Example 1: Replacing the root data model + +```json +{ + "dataModelUpdate": { + "contents": { + "user": { "name": "Alice" }, + "posts": [] + } + } +} +``` + +#### Example 2: Updating a specific path + +```json +{ + "dataModelUpdate": { + "path": "user.name", + "contents": "Bob" + } +} +``` + +### 4.2. Data Binding (The `BoundValue` Object) + +Components connect to the data model through binding. Any property that can be data-bound (like `text` on a `Text` component) accepts an object that defines either a literal value or a data path. + +From `gulf_schema.json`, a bound `text` property looks like this: + +```json +"text": { + "properties": { + "path": { "type": "string" }, + "literalString": { "type": "string" } + } +} +``` + +A component can also bind to numbers (`literalNumber`), booleans (`literalBoolean`), or arrays (`literalArray`). + +- If `literalString` is provided, the value is static. + + ```json + "text": { "literalString": "Hello" } + ``` + +- If `path` is provided, the value is dynamic and resolved from the data model. + + ```json + "text": { "path": "user.name" } + ``` + +The client's interpreter is responsible for resolving these paths against the data model before rendering. The GULF protocol supports direct 1:1 binding; it does not include transformers (e.g., formatters, conditionals). Any data transformation must be performed by the server before sending it in a `dataModelUpdate`. + +## Section 5: Event Handling + +While the server-to-client UI definition is a one-way stream (e.g., over SSE), user interactions are communicated back to the server using a separate, out-of-band mechanism. This is typically a standard REST API endpoint where the client sends a `POST` request. + +### 5.1. The `ClientEvent` Message + +When a user interacts with a component (like a `Button`), the client is responsible for constructing and sending a `ClientEvent` message as a JSON payload to a pre-configured API endpoint. + +The `ClientEvent` object has the following structure: + +- `actionName` (string, required): The name of the action, taken directly from the `action.action` property of the component (e.g., "submit_form"). +- `sourceComponentId` (string, required): The `id` of the component that triggered the event (e.g., "my_button"). +- `timestamp` (string, required): An ISO 8601 timestamp of when the event occurred (e.g., "2025-09-19T17:01:00Z"). +- `resolvedContext` (object, required): A JSON object containing the key-value pairs from the component's `action.context`, after resolving all `BoundValue`s against the data model. + +### 5.2. Resolving the `action.context` + +The `action` property on components like `Button` (as defined in `gulf_schema.json`) contains an optional `context` array. + +```json +"action": { + "description": "Represents a user-initiated action.", + "properties": { + "action": { "type": "string" }, + "context": { + "type": "array", + "items": { + "properties": { + "key": { "type": "string" }, + "value": { + "description": "A BoundValue (path or literal)" + ... + } + } + } + } + } +} +``` + +Before sending the `ClientEvent`, the client **must** iterate over this `context` array and build the `resolvedContext` object. + +1. For each item in the `context` array: +2. Use the item's `key` as the key in the `resolvedContext` object. +3. Resolve the item's `value` (a `BoundValue`): + + - If it's a `literalString`, `literalNumber`, etc., use that value directly. + - If it's a `path`, resolve that path against the current data model. + - If the path is invalid, the value should be `null`. + +### 5.3. Event Flow Example + +1. **Component Definition** (from `componentUpdate`): + + ```json + { + "id": "submit_btn", + "componentProperties": { + "Button": { + "label": { "literalString": "Submit" }, + "action": { + "action": "submit_form", + "context": [ + { "key": "userInput", "value": { "path": "form.textField" } }, + { "key": "formId", "value": { "literalString": "f-123" } } + ] + } + } + } + } + ``` + +2. **Data Model** (from `dataModelUpdate`): + + ```json + { + "form": { + "textField": "User input text" + } + } + ``` + +3. **User Action:** The user taps the "submit_btn" button. +4. **Client-Side Resolution:** The client resolves the `action.context`: + + - `userInput`: Resolves `form.textField` to `"User input text"`. + - `formId`: Resolves `literalString` to `"f-123"`. + +5. **Client-to-Server Request:** The client sends a `POST` request to `https://api.example.com/handle_event` with the following JSON body: + + ```json + { + "actionName": "submit_form", + "sourceComponentId": "submit_btn", + "timestamp": "2025-09-19T17:05:00Z", + "resolvedContext": { + "userInput": "User input text", + "formId": "f-123" + } + } + ``` + +6. **Server Response:** The server processes this event. If the UI needs to change as a result, the server sends new `componentUpdate` or `dataModelUpdate` messages over the **separate SSE stream**. + +## Section 6: Client-Side Implementation + +A robust client-side interpreter for GULF should be composed of several key components: + +- **JSONL Parser:** A parser capable of reading the stream line by line and decoding each line as a separate JSON object. +- **Message Dispatcher:** A mechanism (e.g., a `switch` statement) to identify the message type (`streamHeader`, `componentUpdate`, etc.) and route it to the correct handler. +- **Component Buffer:** A `Map` that stores all component instances by their `id`. This is populated by `componentUpdate` messages. +- **Data Model Store:** A `Map` (or similar) that holds the application state. This is built and modified by `dataModelUpdate` messages. +- **Interpreter State:** A state machine to track if the client is ready to render (e.g., a `_isReadyToRender` boolean that is set to `true` by `beginRendering`). +- **`**WidgetRegistry**`**:\*\*\*\* A developer-provided map (e.g., `Map`) that associates component type strings ("Row", "Text") with functions that build native widgets. +- **Binding Resolver:** A utility that can take a `BoundValue` (e.g., `{ "path": "user.name" }`) and resolve it against the Data Model Store. +- **Event Handler:** A function, exposed to the `WidgetRegistry`, that constructs and sends the `ClientEvent` message to the configured REST API endpoint. + +## Section 7: Complete GULF JSON Schema + +This section provides the formal, consolidated JSON Schema (`gulf_schema.json`) for a single message in the GULF JSONL stream. Each line in the stream must be a valid JSON object that conforms to this schema. + +```json +{ + "title": "A2A UI Protocol Message", + "description": "A single message in the A2A streaming UI protocol. Exactly ONE of the properties in this object must be set, corresponding to the specific message type.", + "type": "object", + "properties": { + "streamHeader": { + "title": "StreamHeader Message", + "description": "A schema for a StreamHeader message in the A2A streaming UI protocol.", + "type": "object", + "properties": { + "version": { + "type": "string", + "description": "The version of the protocol. This property is REQUIRED." + } + }, + "required": ["version"] + }, + "beginRendering": { + "title": "BeginRendering Message", + "description": "A schema for a BeginRendering message in the A2A streaming UI protocol. This message signals that the UI can now be rendered and provides initial root component and styling information.", + "type": "object", + "properties": { + "root": { + "type": "string", + "description": "The ID of the root component from which rendering should begin. This is a reference to a component instance by its unique ID. This property is REQUIRED." + }, + "styles": { + "type": "object", + "description": "An object containing styling information for the UI.", + "properties": { + "font": { + "type": "string", + "description": "The primary font to be used throughout the UI." + }, + "logoUrl": { + "type": "string", + "description": "A URL pointing to the logo image to be displayed." + }, + "primaryColor": { + "type": "string", + "description": "The primary color for the UI, specified as a hexadecimal color code (e.g., '#00BFFF').", + "pattern": "^#[0-9a-fA-F]{6}$" + } + } + } + }, + "required": ["root"] + }, + "componentUpdate": { + "title": "ComponentUpdate Message", + "description": "A schema for a ComponentUpdate message in the A2A streaming UI protocol.", + "type": "object", + "properties": { + "components": { + "type": "array", + "description": "A flat list of all component instances available for rendering. Components reference each other by ID. This property is REQUIRED.", + "items": { + "description": "A specific instance of a ComponentType with its own unique ID and properties.", + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "A unique identifier for this component instance. This property is REQUIRED." + }, + "weight": { + "type": "number", + "description": "The proportional weight of this component within its container. This property may ONLY be used when the component is direct child of either a Row or a Column" + }, + "componentProperties": { + "type": "object", + "description": "Defines the properties for a specific component type. Exactly ONE of the properties in this object must be set.", + "properties": { + "Heading": { + "type": "object", + "properties": { + "text": { + "type": "object", + "properties": { + "path": { + "type": "string", + "description": "A data binding reference to a location in the data model (e.g., '/user/name')." + }, + "literalString": { + "type": "string", + "description": "A fixed, hardcoded string value." + } + } + }, + "level": { + "type": "string", + "enum": ["1", "2", "3", "4", "5"], + "description": "The semantic importance level." + } + }, + "required": ["text"] + }, + "Text": { + "type": "object", + "properties": { + "text": { + "type": "object", + "properties": { + "path": { + "type": "string", + "description": "A data binding reference to a location in the data model (e.g., '/user/name')." + }, + "literalString": { + "type": "string", + "description": "A fixed, hardcoded string value." + } + } + } + }, + "required": ["text"] + }, + "Image": { + "type": "object", + "properties": { + "url": { + "type": "object", + "properties": { + "path": { + "type": "string", + "description": "A data binding reference to a location in the data model (e.g., '/user/name')." + }, + "literalString": { + "type": "string", + "description": "A fixed, hardcoded string value." + } + } + } + }, + "required": ["url"] + }, + "Video": { + "type": "object", + "properties": { + "url": { + "type": "object", + "properties": { + "path": { + "type": "string", + "description": "A data binding reference to a location in the data model (e.g., '/user/name')." + }, + "literalString": { + "type": "string", + "description": "A fixed, hardcoded string value." + } + } + } + }, + "required": ["url"] + }, + "AudioPlayer": { + "type": "object", + "properties": { + "url": { + "type": "object", + "properties": { + "path": { + "type": "string", + "description": "A data binding reference to a location in the data model (e.g., '/user/name')." + }, + "literalString": { + "type": "string", + "description": "A fixed, hardcoded string value." + } + } + }, + "description": { + "type": "object", + "properties": { + "path": { + "type": "string", + "description": "A data binding reference to a location in the data model (e.g., '/user/name')." + }, + "literalString": { + "type": "string", + "description": "A fixed, hardcoded string value." + } + }, + "description": "A label, title, or placeholder text." + } + }, + "required": ["url"] + }, + "Row": { + "type": "object", + "properties": { + "children": { + "type": "object", + "description": "Defines the children of the container. You MUST define EITHER 'explicitList' OR 'template', but not both.", + "properties": { + "explicitList": { + "type": "array", + "description": "An explicit list of component instance IDs.", + "items": { + "type": "string", + "description": "A reference to a component instance by its unique ID." + } + }, + "template": { + "type": "object", + "description": "A template to be rendered for each item in a data-bound list.", + "properties": { + "componentId": { + "type": "string", + "description": "The ID of the component (from the main 'components' list) to use as a template for each item." + }, + "dataBinding": { + "type": "string", + "description": "A data binding reference to a list within the data model (e.g., '/user/posts')." + } + }, + "required": ["componentId", "dataBinding"] + } + } + }, + "distribution": { + "type": "string", + "enum": [ + "start", + "center", + "end", + "spaceBetween", + "spaceAround", + "spaceEvenly" + ], + "description": "Distribution of items along the main axis." + }, + "alignment": { + "type": "string", + "enum": ["start", "center", "end", "stretch"], + "description": "Alignment of items/child along the cross axis." + } + }, + "required": ["children"] + }, + "Column": { + "type": "object", + "properties": { + "children": { + "type": "object", + "description": "Defines the children of the container. You MUST define EITHER 'explicitList' OR 'template', but not both.", + "properties": { + "explicitList": { + "type": "array", + "description": "An explicit list of component instance IDs.", + "items": { + "type": "string", + "description": "A reference to a component instance by its unique ID." + } + }, + "template": { + "type": "object", + "description": "A template to be rendered for each item in a data-bound list.", + "properties": { + "componentId": { + "type": "string", + "description": "The ID of the component (from the main 'components' list) to use as a template for each item." + }, + "dataBinding": { + "type": "string", + "description": "A data binding reference to a list within the data model (e.g., '/user/posts')." + } + }, + "required": ["componentId", "dataBinding"] + } + } + }, + "distribution": { + "type": "string", + "enum": [ + "start", + "center", + "end", + "spaceBetween", + "spaceAround", + "spaceEvenly" + ], + "description": "Distribution of items along the main axis." + }, + "alignment": { + "type": "string", + "enum": ["start", "center", "end", "stretch"], + "description": "Alignment of items/child along the cross axis." + } + }, + "required": ["children"] + }, + "List": { + "type": "object", + "properties": { + "children": { + "type": "object", + "description": "Defines the children of the container. You MUST define EITHER 'explicitList' OR 'template', but not both.", + "properties": { + "explicitList": { + "type": "array", + "description": "An explicit list of component instance IDs.", + "items": { + "type": "string", + "description": "A reference to a component instance by its unique ID." + } + }, + "template": { + "type": "object", + "description": "A template to be rendered for each item in a data-bound list.", + "properties": { + "componentId": { + "type": "string", + "description": "The ID of the component (from the main 'components' list) to use as a template for each item." + }, + "dataBinding": { + "type": "string", + "description": "A data binding reference to a list within the data model (e.g., '/user/posts')." + } + }, + "required": ["componentId", "dataBinding"] + } + } + }, + "direction": { + "type": "string", + "enum": ["vertical", "horizontal"], + "default": "vertical", + "description": "The direction of the list." + }, + "alignment": { + "type": "string", + "enum": ["start", "center", "end", "stretch"], + "description": "Alignment of items/child along the cross axis." + } + }, + "required": ["children"] + }, + "Card": { + "type": "object", + "properties": { + "child": { + "type": "string", + "description": "A reference to a single component instance by its unique ID." + } + }, + "required": ["child"] + }, + "Tabs": { + "type": "object", + "properties": { + "tabItems": { + "type": "array", + "description": "A list of tabs, each with a title and a child component ID.", + "items": { + "type": "object", + "properties": { + "title": { + "type": "object", + "properties": { + "path": { + "type": "string", + "description": "A data binding reference to a location in the data model (e.g., '/user/name')." + }, + "literalString": { + "type": "string", + "description": "A fixed, hardcoded string value." + } + }, + "description": "The title of the tab." + }, + "child": { + "type": "string", + "description": "A reference to a component instance by its unique ID." + } + }, + "required": ["title", "child"] + } + } + }, + "required": ["tabItems"] + }, + "Divider": { + "type": "object", + "properties": { + "axis": { + "type": "string", + "enum": ["horizontal", "vertical"], + "default": "horizontal", + "description": "The orientation." + } + } + }, + "Modal": { + "type": "object", + "properties": { + "entryPointChild": { + "type": "string", + "description": "The ID of the component (e.g., a button) that triggers the modal." + }, + "contentChild": { + "type": "string", + "description": "The ID of the component to display as the modal's content." + } + }, + "required": ["entryPointChild", "contentChild"] + }, + "Button": { + "type": "object", + "properties": { + "label": { + "type": "object", + "properties": { + "path": { + "type": "string", + "description": "A data binding reference to a location in the data model (e.g., '/user/name')." + }, + "literalString": { + "type": "string", + "description": "A fixed, hardcoded string value." + } + } + }, + "action": { + "type": "object", + "description": "Represents a user-initiated action.", + "properties": { + "action": { + "type": "string", + "description": "A unique name identifying the action (e.g., 'submitForm')." + }, + "context": { + "type": "array", + "description": "A key-value map of data bindings to be resolved when the action is triggered.", + "items": { + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "value": { + "type": "object", + "description": "The dynamic value. Define EXACTLY ONE of the nested properties.", + "properties": { + "path": { + "type": "string", + "description": "A data binding reference to a location in the data model (e.g., '/user/name')." + }, + "literalString": { + "type": "string", + "description": "A fixed, hardcoded string value." + }, + "literalNumber": { + "type": "number" + }, + "literalBoolean": { + "type": "boolean" + } + } + } + }, + "required": ["key", "value"] + } + } + }, + "required": ["action"] + } + }, + "required": ["label", "action"] + }, + "CheckBox": { + "type": "object", + "properties": { + "label": { + "type": "object", + "properties": { + "path": { + "type": "string", + "description": "A data binding reference to a location in the data model (e.g., '/user/name')." + }, + "literalString": { + "type": "string", + "description": "A fixed, hardcoded string value." + } + } + }, + "value": { + "type": "object", + "properties": { + "path": { + "type": "string", + "description": "A data binding reference to a location in the data model (e.g., '/user/name')." + }, + "literalBoolean": { + "type": "boolean" + } + } + } + }, + "required": ["label", "value"] + }, + "TextField": { + "type": "object", + "properties": { + "text": { + "type": "object", + "properties": { + "path": { + "type": "string", + "description": "A data binding reference to a location in the data model (e.g., '/user/name')." + }, + "literalString": { + "type": "string", + "description": "A fixed, hardcoded string value." + } + } + }, + "label": { + "type": "object", + "properties": { + "path": { + "type": "string", + "description": "A data binding reference to a location in the data model (e.g., '/user/name')." + }, + "literalString": { + "type": "string", + "description": "A fixed, hardcoded string value." + } + }, + "description": "A label, title, or placeholder text." + }, + "type": { + "type": "string", + "enum": ["shortText", "number", "date", "longText"] + }, + "validationRegexp": { + "type": "string", + "description": "A regex string to validate the input." + } + }, + "required": ["label"] + }, + "DateTimeInput": { + "type": "object", + "properties": { + "value": { + "type": "object", + "properties": { + "path": { + "type": "string", + "description": "A data binding reference to a location in the data model (e.g., '/user/name')." + }, + "literalString": { + "type": "string", + "description": "A fixed, hardcoded string value." + } + } + }, + "enableDate": { + "type": "boolean", + "default": true + }, + "enableTime": { + "type": "boolean", + "default": false + }, + "outputFormat": { + "type": "string", + "description": "The string format for the output (e.g., 'YYYY-MM-DD')." + } + }, + "required": ["value"] + }, + "MultipleChoice": { + "type": "object", + "properties": { + "selections": { + "type": "object", + "properties": { + "path": { + "type": "string", + "description": "A data binding reference to a location in the data model (e.g., '/user/name')." + }, + "literalArray": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "options": { + "type": "array", + "items": { + "type": "object", + "properties": { + "label": { + "type": "object", + "properties": { + "path": { + "type": "string", + "description": "A data binding reference to a location in the data model (e.g., '/user/name')." + }, + "literalString": { + "type": "string", + "description": "A fixed, hardcoded string value." + } + } + }, + "value": { + "type": "string" + } + }, + "required": ["label", "value"] + } + }, + "maxAllowedSelections": { + "type": "integer", + "default": 1 + } + }, + "required": ["selections"] + }, + "Slider": { + "type": "object", + "properties": { + "value": { + "type": "object", + "properties": { + "path": { + "type": "string", + "description": "A data binding reference to a location in the data model (e.g., '/user/name')." + }, + "literalNumber": { + "type": "number" + } + } + }, + "minValue": { + "type": "number", + "default": 0 + }, + "maxValue": { + "type": "number", + "default": 100 + } + }, + "required": ["value"] + } + } + } + }, + "required": ["id", "componentProperties"] + } + } + }, + "required": ["components"] + }, + "dataModelUpdate": { + "title": "Data model update", + "description": "Sets or replaces the data model at a specified path with new content.", + "type": "object", + "properties": { + "path": { + "type": "string", + "description": "An optional path to a location within the data model where the content should be inserted or replaced. The path is represented as a dot-separated string and can include array indexing (e.g., 'user.addresses[0].street'). If this field is omitted, the entire data model will be replaced with the provided 'contents'." + }, + "contents": { + "description": "The JSON content to be placed at the specified path. This property is REQUIRED. This can be any valid JSON value (object, array, string, number, boolean, or null). The content at the target path will be completely replaced by this new value." + } + }, + "required": ["contents"] + } + } +} +``` From b9a1bbc3586288e1b8432729dc2fcf4c5ca5a783 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 24 Sep 2025 17:20:28 +0000 Subject: [PATCH 2/2] Bump actions/cache from 4.2.4 to 4.3.0 Bumps [actions/cache](https://github.com/actions/cache) from 4.2.4 to 4.3.0. - [Release notes](https://github.com/actions/cache/releases) - [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md) - [Commits](https://github.com/actions/cache/compare/0400d5f644dc74513175e3cd8d07132dd4860809...0057852bfaa89a56745cba8c7296529d2fc39830) --- updated-dependencies: - dependency-name: actions/cache dependency-version: 4.3.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/flutter_packages.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/flutter_packages.yaml b/.github/workflows/flutter_packages.yaml index c06eb288a..c221bc56b 100644 --- a/.github/workflows/flutter_packages.yaml +++ b/.github/workflows/flutter_packages.yaml @@ -113,7 +113,7 @@ jobs: - name: Print Flutter version run: flutter --version - name: Cache Pub dependencies - uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 with: path: ${{ env.PUB_CACHE }} key: ${{ runner.os }}-pub-${{ hashFiles('**/pubspec.lock') }}