From f81a65762b88b4349e9f58c0d7f9bbae3bfc157e Mon Sep 17 00:00:00 2001 From: Greg Spencer Date: Wed, 28 Jan 2026 14:22:34 -0800 Subject: [PATCH 1/7] Revert "feat(protocol)!: refactor FunctionCall args to positional array (#515)" This reverts commit d11883fc5120c6a0b9e404d59a8046b008e42f99. --- specification/v0_9/docs/a2ui_protocol.md | 79 +++++----- specification/v0_9/json/common_types.json | 144 +++++++++++++----- specification/v0_9/json/standard_catalog.json | 79 ++++++---- .../v0_9/test/cases/button_checks.json | 8 +- .../v0_9/test/cases/checkable_components.json | 18 ++- .../test/cases/contact_form_example.jsonl | 2 +- .../test/cases/contact_form_example_test.json | 1 - 7 files changed, 219 insertions(+), 112 deletions(-) diff --git a/specification/v0_9/docs/a2ui_protocol.md b/specification/v0_9/docs/a2ui_protocol.md index 1466e1441..b4ffba63b 100644 --- a/specification/v0_9/docs/a2ui_protocol.md +++ b/specification/v0_9/docs/a2ui_protocol.md @@ -88,8 +88,8 @@ To support A2UI, a transport layer must fulfill the following contract: 1. **Reliable delivery**: Messages must be delivered in the order they were generated. A2UI relies on stateful updates (e.g., creating a surface before updating it), so out-of-order delivery can corrupt the UI state. 2. **Message framing**: The transport must clearly delimit individual JSON envelope messages (e.g., using newlines in JSONL, WebSocket frames, or SSE events). 3. **Metadata support**: The transport must provide a mechanism to associate metadata with messages. This is critical for: - * **Data model synchronization**: The `sendDataModel` feature requires the client to send the current data model state as metadata alongside user actions. - * **Capabilities exchange**: Client capabilities (supported catalogs, custom components) are exchanged via metadata. + - **Data model synchronization**: The `sendDataModel` feature requires the client to send the current data model state as metadata alongside user actions. + - **Capabilities exchange**: Client capabilities (supported catalogs, custom components) are exchanged via metadata. 4. **Bidirectional capability (optional)**: While the rendering stream is unidirectional (Server -> Client), interactive applications require a return channel for `action` messages (Client -> Server). ### Transport bindings @@ -101,16 +101,16 @@ While A2UI is agnostic, it is most commonly used with the following transports. [A2A (Agent-to-Agent)](https://a2a-protocol.org/latest/) is an excellent transport option for A2UI in agentic systems, extending A2A with additional payloads. A2A is uniquely capable of handling remote agent communication, and can also provide a secure and effecient transport between an agentic backend and front end application. -* **Message mapping**: Each A2UI envelope (e.g., `updateComponents`) corresponds to the payload of a single A2A message Part. -* **Metadata**: - * **Data model**: When `sendDataModel` is active, the client's `a2uiClientDataModel` object is placed in the `metadata` field of the A2A message. - * **Capabilities**: The `a2uiClientCapabilities` object is placed in the `metadata` field of every A2A message sent from the client to the server. -* **Context**: A2UI sessions typically map to A2A `contextId`. All messages for a set of related surfaces should share the same `contextId`. +- **Message mapping**: Each A2UI envelope (e.g., `updateComponents`) corresponds to the payload of a single A2A message Part. +- **Metadata**: + - **Data model**: When `sendDataModel` is active, the client's `a2uiClientDataModel` object is placed in the `metadata` field of the A2A message. + - **Capabilities**: The `a2uiClientCapabilities` object is placed in the `metadata` field of every A2A message sent from the client to the server. +- **Context**: A2UI sessions typically map to A2A `contextId`. All messages for a set of related surfaces should share the same `contextId`. #### AG UI (Agent to User Interface) binding **[AG-UI](https://docs.ag-ui.com/introduction)** is also an excellent transport option for A2UI Agent–User Interaction protocol. -AG UI provides convenient integrations into many agent frameworks and frontends. AG UI provides low latency and shared state message passing between front ends and agentic backends. +AG UI provides convenient integrations into many agent frameworks and frontends. AG UI provides low latency and shared state message passing between front ends and agentic backends. #### Other transports @@ -131,7 +131,6 @@ The [`common_types.json`] schema defines reusable primitives used throughout the - **`DynamicString` / `DynamicNumber` / `DynamicBoolean` / `DynamicStringList`**: The core of the data binding system. Any property that can be bound to data is defined as a `Dynamic*` type. It accepts either a literal value, a `path` string ([JSON Pointer]), or a `FunctionCall` (function call). - **`ChildList`**: Defines how containers hold children. It supports: - - `array`: A static array of `ComponentId` component references. - `object`: A template for generating children from a data binding list (requires a template `componentId` and a data binding `path`). @@ -163,11 +162,11 @@ Custom catalogs can be used to define additional UI components or modify the beh To ensure that automated validators can verify the integrity of your UI tree (checking that parents reference existing children), custom catalogs MUST adhere to the following strict typing rules: 1. **Single child references:** Any property that holds the ID of another component MUST use the `ComponentId` type defined in `common_types.json`. - * Use: `"$ref": "common_types.json#/$defs/ComponentId"` - * Do NOT use: `"type": "string"` + - Use: `"$ref": "common_types.json#/$defs/ComponentId"` + - Do NOT use: `"type": "string"` 2. **List references:** Any property that holds a list of children or a template MUST use the `ChildList` type. - * Use: `"$ref": "common_types.json#/$defs/ChildList"` + - Use: `"$ref": "common_types.json#/$defs/ChildList"` Validators determine which fields represent structural links by looking for these specific schema references. If you use a raw string type for an ID, the validator will treat it as static text (like a URL or label) and will not check if the target component exists. @@ -283,8 +282,8 @@ The following example demonstrates a complete interaction to render a Contact Fo ```jsonl {"createSurface":{"surfaceId":"contact_form_1","catalogId":"https://a2ui.dev/specification/v0_9/standard_catalog.json"}} -{"updateComponents":{"surfaceId":"contact_form_1","components":[{"id":"root","component":"Card","child":"form_container"},{"id":"form_container","component":"Column","children":["header_row","name_row","email_group","phone_group","pref_group","divider_1","newsletter_checkbox","submit_button"],"justify":"start","align":"stretch"},{"id":"header_row","component":"Row","children":["header_icon","header_text"],"align":"center"},{"id":"header_icon","component":"Icon","name":"mail"},{"id":"header_text","component":"Text","text":"# Contact Us","variant":"h2"},{"id":"name_row","component":"Row","children":["first_name_group","last_name_group"],"justify":"spaceBetween"},{"id":"first_name_group","component":"Column","children":["first_name_label","first_name_field"],"weight":1},{"id":"first_name_label","component":"Text","text":"First Name","variant":"caption"},{"id":"first_name_field","component":"TextField","label":"First Name","value":{"path":"/contact/firstName"},"variant":"shortText"},{"id":"last_name_group","component":"Column","children":["last_name_label","last_name_field"],"weight":1},{"id":"last_name_label","component":"Text","text":"Last Name","variant":"caption"},{"id":"last_name_field","component":"TextField","label":"Last Name","value":{"path":"/contact/lastName"},"variant":"shortText"},{"id":"email_group","component":"Column","children":["email_label","email_field"]},{"id":"email_label","component":"Text","text":"Email Address","variant":"caption"},{"id":"email_field","component":"TextField","label":"Email","value":{"path":"/contact/email"},"variant":"shortText","checks":[{"call":"required","args":[{"path":"/contact/email"}],"message":"Email is required."},{"call":"email","args":[{"path":"/contact/email"}],"message":"Please enter a valid email address."}]},{"id":"phone_group","component":"Column","children":["phone_label","phone_field"]},{"id":"phone_label","component":"Text","text":"Phone Number","variant":"caption"},{"id":"phone_field","component":"TextField","label":"Phone","value":{"path":"/contact/phone"},"variant":"shortText","checks":[{"call":"regex","args":[{"path":"/contact/phone"}, "^\\d{10}$"],"message":"Phone number must be 10 digits."}]},{"id":"pref_group","component":"Column","children":["pref_label","pref_picker"]},{"id":"pref_label","component":"Text","text":"Preferred Contact Method","variant":"caption"},{"id":"pref_picker","component":"ChoicePicker","variant":"mutuallyExclusive","options":[{"label":"Email","value":"email"},{"label":"Phone","value":"phone"},{"label":"SMS","value":"sms"}],"value":{"path":"/contact/preference"}},{"id":"divider_1","component":"Divider","axis":"horizontal"},{"id":"newsletter_checkbox","component":"CheckBox","label":"Subscribe to our newsletter","value":{"path":"/contact/subscribe"}},{"id":"submit_button_label","component":"Text","text":"Send Message"},{"id":"submit_button","component":"Button","child":"submit_button_label","variant":"primary","action":{"event":{"name":"submitContactForm","context":{"formId":"contact_form_1","clientTime":{"call":"now","returnType":"string"},"isNewsletterSubscribed":{"path":"/contact/subscribe"}}}}}]}} -{"updateDataModel":{"surfaceId":"contact_form_1","path":"/contact","value":{"firstName":"John","lastName":"Doe","email":"john.doe@example.com","phone":"1234567890","preference":["email"],"subscribe":true}}} +{"updateComponents":{"surfaceId":"contact_form_1","components":[{"id":"root","component":"Card","child":"form_container"},{"id":"form_container","component":"Column","children":["header_row","name_row","email_group","phone_group","pref_group","divider_1","newsletter_checkbox","submit_button"],"justify":"start","align":"stretch"},{"id":"header_row","component":"Row","children":["header_icon","header_text"],"align":"center"},{"id":"header_icon","component":"Icon","name":"mail"},{"id":"header_text","component":"Text","text":"# Contact Us","variant":"h2"},{"id":"name_row","component":"Row","children":["first_name_group","last_name_group"],"justify":"spaceBetween"},{"id":"first_name_group","component":"Column","children":["first_name_label","first_name_field"],"weight":1},{"id":"first_name_label","component":"Text","text":"First Name","variant":"caption"},{"id":"first_name_field","component":"TextField","label":"First Name","value":{"path":"/contact/firstName"},"variant":"shortText"},{"id":"last_name_group","component":"Column","children":["last_name_label","last_name_field"],"weight":1},{"id":"last_name_label","component":"Text","text":"Last Name","variant":"caption"},{"id":"last_name_field","component":"TextField","label":"Last Name","value":{"path":"/contact/lastName"},"variant":"shortText"},{"id":"email_group","component":"Column","children":["email_label","email_field"]},{"id":"email_label","component":"Text","text":"Email Address","variant":"caption"},{"id":"email_field","component":"TextField","label":"Email","value":{"path":"/contact/email"},"variant":"shortText","checks":[{"call":"required","message":"Email is required."},{"call":"email","message":"Please enter a valid email address."}]},{"id":"phone_group","component":"Column","children":["phone_label","phone_field"]},{"id":"phone_label","component":"Text","text":"Phone Number","variant":"caption"},{"id":"phone_field","component":"TextField","label":"Phone","value":{"path":"/contact/phone"},"variant":"shortText","checks":[{"call":"regex","args":{"pattern":"^\\d{10}$"},"message":"Phone number must be 10 digits."}]},{"id":"pref_group","component":"Column","children":["pref_label","pref_picker"]},{"id":"pref_label","component":"Text","text":"Preferred Contact Method","variant":"caption"},{"id":"pref_picker","component":"ChoicePicker","variant":"mutuallyExclusive","options":[{"label":"Email","value":"email"},{"label":"Phone","value":"phone"},{"label":"SMS","value":"sms"}],"value":{"path":"/contact/preference"}},{"id":"divider_1","component":"Divider","axis":"horizontal"},{"id":"newsletter_checkbox","component":"CheckBox","label":"Subscribe to our newsletter","value":{"path":"/contact/subscribe"}},{"id":"submit_button_label","component":"Text","text":"Send Message"},{"id":"submit_button","component":"Button","child":"submit_button_label","variant":"primary","action":{"name":"submitContactForm","context":{"formId":"contact_form_1","clientTime":{"call":"now","returnType":"string"},"isNewsletterSubscribed":{"path":"/contact/subscribe"}}}}]}} +{"updateDataModel":{"surfaceId":"contact_form_1","path":"/contact","value":{"firstName":"John","lastName":"Doe"}}} {"deleteSurface":{"surfaceId":"contact_form_1"}} ``` @@ -404,7 +403,6 @@ When a container component (such as `Column`, `Row`, or `List`) utilizes the **T - **Template definition:** When a container binds its children to a path (e.g., `path: "/users"`), the client iterates over the array found at that location. - **Scope instantiation:** For every item in the array, the client instantiates the template component. - **Relative resolution:** Inside these instantiated components, any path that **does not** start with a forward slash `/` is treated as a **Relative Path**. - - A relative path `firstName` inside a template iterating over `/users` resolves to `/users/0/firstName` for the first item, `/users/1/firstName` for the second, etc. - **Mixing scopes:** Components inside a Child Scope can still access the Root Scope by using an Absolute Path. @@ -570,10 +568,17 @@ _Replace the entire data model:_ When `sendDataModel` is set to `true` for a surface, the client automatically appends the **entire data model** of that surface to the metadata of every message (such as `action` or user query) sent to the server that created the surface. The data model is included using the transport's metadata facility (e.g., the `metadata` field in A2A or a header in HTTP). The payload follows the schema in [`a2ui_client_data_model.json`](../json/a2ui_client_data_model.json). +<<<<<<< HEAD + - **Targeted Delivery**: The data model is sent exclusively to the server that created the surface. Data cannot leak to other agents or servers. - **Trigger**: Data is sent only when a client-to-server message is triggered (e.g., by a user action like a button click). Passive data changes (like typing in a text field) do not trigger a network request on their own; they simply update the local state, which will be sent with the next action. - **Payload**: The data model is included in the transport metadata, tagged by its `surfaceId`. -- **Convergence**: The server treats the received data model as the current state of the client at the time of the action. +- # **Convergence**: The server treats the received data model as the current state of the client at the time of the action. +- **Targeted Delivery**: The data model is sent exclusively to the server that created the surface. Data cannot leak to other agents or servers. +- **Trigger:** Data is sent only when an A2A message is triggered (e.g., by a user action like a button click). Passive data changes (like typing in a text field) do not trigger a network request on their own; they simply update the local state, which will be sent with the next action. +- **Payload:** The data model is included in the A2A message metadata, tagged by its `surfaceId`. +- **Convergence:** The server treats the received data model as the current state of the client at the time of the action. + > > > > > > > parent of d11883f (feat(protocol)!: refactor FunctionCall args to positional array (#515)) ## Client-side logic & validation @@ -589,15 +594,15 @@ Input components (like `TextField`, `CheckBox`) can define a list of checks. Eac "checks": [ { "call": "required", - "args": [{ "path": "/formData/zip" }], + "args": { "value": { "path": "/formData/zip" } }, "message": "Zip code is required" }, { "call": "regex", - "args": [ - { "path": "/formData/zip" }, - "^[0-9]{5}$" - ], + "args": { + "value": { "path": "/formData/zip" }, + "pattern": "^[0-9]{5}$" + }, "message": "Must be a 5-digit zip code" } ] @@ -616,17 +621,17 @@ Buttons can also define `checks`. If any check fails, the button is automaticall "and": [ { "call": "required", - "args": [{ "path": "/formData/terms" }] + "args": { "value": { "path": "/formData/terms" } } }, { "or": [ { "call": "required", - "args": [{ "path": "/formData/email" }] + "args": { "value": { "path": "/formData/email" } } }, { "call": "required", - "args": [{ "path": "/formData/phone" }] + "args": { "value": { "path": "/formData/phone" } } } ] } @@ -666,15 +671,15 @@ The [`standard_catalog.json`] provides the baseline set of components and functi ### Functions -| Function | Description | -| :---------------- | :----------------------------------------------------------------------- | -| **required** | Checks that the value is not null, undefined, or empty. | -| **regex** | Checks that the value matches a regular expression string. | -| **length** | Checks string length constraints. | -| **numeric** | Checks numeric range constraints. | -| **email** | Checks that the value is a valid email address. | -| **formatString** | Does string interpolation of data model values and registered functions. | -| **openUrl** | Opens a URL in a browser. | +| Function | Description | +| :--------------- | :----------------------------------------------------------------------- | +| **required** | Checks that the value is not null, undefined, or empty. | +| **regex** | Checks that the value matches a regular expression string. | +| **length** | Checks string length constraints. | +| **numeric** | Checks numeric range constraints. | +| **email** | Checks that the value is a valid email address. | +| **formatString** | Does string interpolation of data model values and registered functions. | +| **openUrl** | Opens a URL in a browser. | ### Theme @@ -714,10 +719,17 @@ Values from the data model can be interpolated using their JSON Pointer path. "id": "user_welcome", "component": "Text", "text": { +<<<<<<< HEAD "call": "formatString", "args": [ "Hello, ${/user/firstName}! Welcome back to ${/appName}." ] +======= + "call": "string_format", + "args": { + "value": "Hello, ${/user/firstName}! Welcome back to ${/appName}." + } +>>>>>>> parent of d11883f (feat(protocol)!: refactor FunctionCall args to positional array (#515)) } } ``` @@ -751,7 +763,6 @@ When a non-string value is interpolated, the client converts it to a string: The A2UI protocol is designed to be used in a three-step loop with a Large Language Model: 1. **Prompt**: Construct a prompt for the LLM that includes: - - The desired UI to be generated. - The A2UI JSON schema, including the component catalog. - Examples of valid A2UI JSON. diff --git a/specification/v0_9/json/common_types.json b/specification/v0_9/json/common_types.json index 2a80dbdf0..e1c7f7b22 100644 --- a/specification/v0_9/json/common_types.json +++ b/specification/v0_9/json/common_types.json @@ -60,37 +60,60 @@ } ] }, - "DataBinding": { - "type": "object", - "properties": { - "path": { - "type": "string", - "description": "A JSON Pointer path to a value in the data model." - } - }, - "required": ["path"], - "additionalProperties": false - }, "DynamicValue": { "description": "A value that can be a literal, a path, or a function call returning any type.", "oneOf": [ - { "type": "string" }, - { "type": "number" }, - { "type": "boolean" }, - { "$ref": "#/$defs/DataBinding" }, - { "$ref": "#/$defs/FunctionCall" } + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "type": "object", + "properties": { + "path": { + "type": "string" + } + }, + "required": ["path"], + "additionalProperties": false + }, + { + "$ref": "#/$defs/FunctionCall" + } ] }, "DynamicString": { "description": "Represents a string", "oneOf": [ - { "type": "string" }, - { "$ref": "#/$defs/DataBinding" }, + { + "type": "string" + }, + { + "type": "object", + "properties": { + "path": { + "type": "string" + } + }, + "required": ["path"], + "additionalProperties": false + }, { "allOf": [ - { "$ref": "#/$defs/FunctionCall" }, { - "properties": { "returnType": { "const": "string" } }, + "$ref": "#/$defs/FunctionCall" + }, + { + "properties": { + "returnType": { + "const": "string" + } + }, "required": ["returnType"] } ] @@ -100,13 +123,30 @@ "DynamicNumber": { "description": "Represents a value that can be either a literal number, a path to a number in the data model, or a function call returning a number.", "oneOf": [ - { "type": "number" }, - { "$ref": "#/$defs/DataBinding" }, + { + "type": "number" + }, + { + "type": "object", + "properties": { + "path": { + "type": "string" + } + }, + "required": ["path"], + "additionalProperties": false + }, { "allOf": [ - { "$ref": "#/$defs/FunctionCall" }, { - "properties": { "returnType": { "const": "number" } }, + "$ref": "#/$defs/FunctionCall" + }, + { + "properties": { + "returnType": { + "const": "number" + } + }, "required": ["returnType"] } ] @@ -116,9 +156,22 @@ "DynamicBoolean": { "description": "A boolean value that can be a literal, a path, or a logic expression (including function calls returning boolean).", "oneOf": [ - { "type": "boolean" }, - { "$ref": "#/$defs/DataBinding" }, - { "$ref": "#/$defs/LogicExpression" } + { + "type": "boolean" + }, + { + "type": "object", + "properties": { + "path": { + "type": "string" + } + }, + "required": ["path"], + "additionalProperties": false + }, + { + "$ref": "#/$defs/LogicExpression" + } ] }, "DynamicStringList": { @@ -126,14 +179,31 @@ "oneOf": [ { "type": "array", - "items": { "type": "string" } + "items": { + "type": "string" + } + }, + { + "type": "object", + "properties": { + "path": { + "type": "string" + } + }, + "required": ["path"], + "additionalProperties": false }, - { "$ref": "#/$defs/DataBinding" }, { "allOf": [ - { "$ref": "#/$defs/FunctionCall" }, { - "properties": { "returnType": { "const": "array" } }, + "$ref": "#/$defs/FunctionCall" + }, + { + "properties": { + "returnType": { + "const": "array" + } + }, "required": ["returnType"] } ] @@ -149,16 +219,10 @@ "description": "The name of the function to call." }, "args": { - "type": "array", + "type": "object", "description": "Arguments passed to the function.", - "items": { - "anyOf": [ - { "$ref": "#/$defs/DynamicValue" }, - { - "type": "object", - "description": "A literal object argument (e.g. configuration)." - } - ] + "additionalProperties": { + "$ref": "#/$defs/DynamicValue" } }, "returnType": { diff --git a/specification/v0_9/json/standard_catalog.json b/specification/v0_9/json/standard_catalog.json index 77722f081..76f2b8992 100644 --- a/specification/v0_9/json/standard_catalog.json +++ b/specification/v0_9/json/standard_catalog.json @@ -654,9 +654,8 @@ "description": "Checks that the value is not null, undefined, or empty.", "returnType": "boolean", "parameters": { - "type": "array", - "items": [{ "$ref": "common_types.json#/$defs/DynamicValue" }], - "minItems": 1 + "allOf": [{ "$ref": "#/$defs/valueParam" }], + "unevaluatedProperties": false } }, { @@ -664,15 +663,21 @@ "description": "Checks that the value matches a regular expression string.", "returnType": "boolean", "parameters": { - "type": "array", - "items": [ - { "$ref": "common_types.json#/$defs/DynamicValue" }, + "allOf": [ + { "$ref": "#/$defs/valueParam" }, { - "type": "string", - "description": "The regex pattern to match against." + "type": "object", + "properties": { + "value": { "type": "string" }, + "pattern": { + "type": "string", + "description": "The regex pattern to match against." + } + }, + "required": ["pattern"] } ], - "minItems": 2 + "unevaluatedProperties": false } }, { @@ -680,12 +685,12 @@ "description": "Checks string length constraints.", "returnType": "boolean", "parameters": { - "type": "array", - "items": [ - { "$ref": "common_types.json#/$defs/DynamicValue" }, + "allOf": [ + { "$ref": "#/$defs/valueParam" }, { "type": "object", "properties": { + "value": { "type": "string" }, "min": { "type": "integer", "minimum": 0, @@ -700,7 +705,7 @@ "anyOf": [{ "required": ["min"] }, { "required": ["max"] }] } ], - "minItems": 2 + "unevaluatedProperties": false } }, { @@ -708,12 +713,12 @@ "description": "Checks numeric range constraints.", "returnType": "boolean", "parameters": { - "type": "array", - "items": [ - { "$ref": "common_types.json#/$defs/DynamicValue" }, + "allOf": [ + { "$ref": "#/$defs/valueParam" }, { "type": "object", "properties": { + "value": { "type": "number" }, "min": { "type": "number", "description": "The minimum allowed value." @@ -726,7 +731,7 @@ "anyOf": [{ "required": ["min"] }, { "required": ["max"] }] } ], - "minItems": 2 + "unevaluatedProperties": false } }, { @@ -734,9 +739,16 @@ "description": "Checks that the value is a valid email address.", "returnType": "boolean", "parameters": { - "type": "array", - "items": [{ "$ref": "common_types.json#/$defs/DynamicValue" }], - "minItems": 1 + "allOf": [ + { "$ref": "#/$defs/valueParam" }, + { + "type": "object", + "properties": { + "value": { "type": "string" } + } + } + ], + "unevaluatedProperties": false } }, { @@ -744,10 +756,16 @@ "description": "Performs string interpolation of data model values and other functions in the catalog functions list and returns the resulting string. The value string can contain interpolated expressions in the `${expression}` format. Supported expression types include: JSON Pointer paths to the data model (e.g., `${/absolute/path}` or `${relative/path}`), and client-side function calls (e.g., `${now()}`). Function arguments must be literals (quoted strings, numbers, booleans) or nested expressions (e.g., `${formatDate(${/currentDate}, 'MM-dd')}`). To include a literal `${` sequence, escape it as `\\${`.", "returnType": "string", "parameters": { - "type": "array", - "items": [{ "description": "The format string.", "type": "string" }], - "additionalItems": { "$ref": "common_types.json#/$defs/DynamicValue" }, - "minItems": 1 + "allOf": [ + { "$ref": "#/$defs/valueParam" }, + { + "type": "object", + "properties": { + "value": { "type": "string" } + } + } + ], + "unevaluatedProperties": false } }, { @@ -903,7 +921,7 @@ "CatalogComponentCommon": { "type": "object", "properties": { - "weight": { + "weight": { "type": "number", "description": "The relative weight of this component within a Row or Column. This is similar to the CSS 'flex-grow' property. Note: this may ONLY be set when the component is a direct descendant of a Row or Column." } @@ -933,6 +951,15 @@ "discriminator": { "propertyName": "component" } + }, + "valueParam": { + "type": "object", + "properties": { + "value": { + "description": "The value to check." + } + }, + "required": ["value"] } } -} +} \ No newline at end of file diff --git a/specification/v0_9/test/cases/button_checks.json b/specification/v0_9/test/cases/button_checks.json index 2e957f815..91a5e93af 100644 --- a/specification/v0_9/test/cases/button_checks.json +++ b/specification/v0_9/test/cases/button_checks.json @@ -18,19 +18,19 @@ "and": [ { "call": "required", - "args": [{ "path": "/formData/terms" }], + "args": { "value": { "path": "/formData/terms" } }, "returnType": "boolean" }, { "or": [ { "call": "required", - "args": [{ "path": "/formData/email" }], + "args": { "value": { "path": "/formData/email" } }, "returnType": "boolean" }, { "call": "required", - "args": [{ "path": "/formData/phone" }], + "args": { "value": { "path": "/formData/phone" } }, "returnType": "boolean" } ] @@ -79,7 +79,7 @@ "checks": [ { "call": "required", - "args": ["foo"], + "args": { "value": "foo" }, "returnType": "not_a_valid_type" } ] diff --git a/specification/v0_9/test/cases/checkable_components.json b/specification/v0_9/test/cases/checkable_components.json index 7b9b1a810..9a0f1a93d 100644 --- a/specification/v0_9/test/cases/checkable_components.json +++ b/specification/v0_9/test/cases/checkable_components.json @@ -16,13 +16,13 @@ "checks": [ { "call": "required", - "args": [{ "path": "/formData/email" }], + "args": { "value": { "path": "/formData/email" } }, "returnType": "boolean", "message": "Email is required" }, { "call": "email", - "args": [{ "path": "/formData/email" }], + "args": { "value": { "path": "/formData/email" } }, "returnType": "boolean", "message": "Must be valid email" } @@ -52,7 +52,10 @@ "checks": [ { "call": "length", - "args": [{ "path": "/formData/interests" }, { "min": 1 }], + "args": { + "value": { "path": "/formData/interests" }, + "min": 1 + }, "returnType": "boolean", "message": "Select at least one" } @@ -79,7 +82,10 @@ "checks": [ { "call": "numeric", - "args": [{ "path": "/formData/rating" }, { "min": 3 }], + "args": { + "value": { "path": "/formData/rating" }, + "min": 3 + }, "returnType": "boolean", "message": "Rating must be > 3" } @@ -104,7 +110,7 @@ "checks": [ { "call": "required", - "args": [{ "path": "/formData/agreed" }], + "args": { "value": { "path": "/formData/agreed" } }, "returnType": "boolean", "message": "Must agree" } @@ -129,7 +135,7 @@ "checks": [ { "call": "required", - "args": [{ "path": "/formData/date" }], + "args": { "value": { "path": "/formData/date" } }, "returnType": "boolean", "message": "Date required" } diff --git a/specification/v0_9/test/cases/contact_form_example.jsonl b/specification/v0_9/test/cases/contact_form_example.jsonl index 146a12908..43a1c6dca 100644 --- a/specification/v0_9/test/cases/contact_form_example.jsonl +++ b/specification/v0_9/test/cases/contact_form_example.jsonl @@ -1,4 +1,4 @@ {"createSurface":{"surfaceId":"contact_form_1","catalogId":"https://a2ui.dev/specification/v0_9/standard_catalog.json"}} -{"updateComponents":{"surfaceId":"contact_form_1","components":[{"id":"root","component":"Card","child":"form_container"},{"id":"form_container","component":"Column","children":["header_row","name_row","email_group","phone_group","pref_group","divider_1","newsletter_checkbox","submit_button"],"justify":"start","align":"stretch"},{"id":"header_row","component":"Row","children":["header_icon","header_text"],"align":"center"},{"id":"header_icon","component":"Icon","name":"mail"},{"id":"header_text","component":"Text","text":"# Contact Us","variant":"h2"},{"id":"name_row","component":"Row","children":["first_name_group","last_name_group"],"justify":"spaceBetween"},{"id":"first_name_group","component":"Column","children":["first_name_label","first_name_field"],"weight":1},{"id":"first_name_label","component":"Text","text":"First Name","variant":"caption"},{"id":"first_name_field","component":"TextField","label":"First Name","value":{"path":"/contact/firstName"},"variant":"shortText"},{"id":"last_name_group","component":"Column","children":["last_name_label","last_name_field"],"weight":1},{"id":"last_name_label","component":"Text","text":"Last Name","variant":"caption"},{"id":"last_name_field","component":"TextField","label":"Last Name","value":{"path":"/contact/lastName"},"variant":"shortText"},{"id":"email_group","component":"Column","children":["email_label","email_field"]},{"id":"email_label","component":"Text","text":"Email Address","variant":"caption"},{"id":"email_field","component":"TextField","label":"Email","value":{"path":"/contact/email"},"variant":"shortText","checks":[{"call":"required","args":[{"path":"/contact/email"}],"message":"Email is required."},{"call":"email","args":[{"path":"/contact/email"}],"message":"Please enter a valid email address."}]},{"id":"phone_group","component":"Column","children":["phone_label","phone_field"]},{"id":"phone_label","component":"Text","text":"Phone Number","variant":"caption"},{"id":"phone_field","component":"TextField","label":"Phone","value":{"path":"/contact/phone"},"variant":"shortText","checks":[{"call":"regex","args":[{"path":"/contact/phone"},"^\\d{10}$"],"message":"Phone number must be 10 digits."}]},{"id":"pref_group","component":"Column","children":["pref_label","pref_picker"]},{"id":"pref_label","component":"Text","text":"Preferred Contact Method","variant":"caption"},{"id":"pref_picker","component":"ChoicePicker","variant":"mutuallyExclusive","options":[{"label":"Email","value":"email"},{"label":"Phone","value":"phone"},{"label":"SMS","value":"sms"}],"value":{"path":"/contact/preference"}},{"id":"divider_1","component":"Divider","axis":"horizontal"},{"id":"newsletter_checkbox","component":"CheckBox","label":"Subscribe to our newsletter","value":{"path":"/contact/subscribe"}},{"id":"submit_button_label","component":"Text","text":"Send Message"},{"id":"submit_button","component":"Button","child":"submit_button_label","variant":"primary","action":{"event":{"name":"submitContactForm","context":{"formId":"contact_form_1","clientTime":{"call":"now","returnType":"string"},"isNewsletterSubscribed":{"path":"/contact/subscribe"}}}}}]}} +{"updateComponents":{"surfaceId":"contact_form_1","components":[{"id":"root","component":"Card","child":"form_container"},{"id":"form_container","component":"Column","children":["header_row","name_row","email_group","phone_group","pref_group","divider_1","newsletter_checkbox","submit_button"],"justify":"start","align":"stretch"},{"id":"header_row","component":"Row","children":["header_icon","header_text"],"align":"center"},{"id":"header_icon","component":"Icon","name":"mail"},{"id":"header_text","component":"Text","text":"# Contact Us","variant":"h2"},{"id":"name_row","component":"Row","children":["first_name_group","last_name_group"],"justify":"spaceBetween"},{"id":"first_name_group","component":"Column","children":["first_name_label","first_name_field"],"weight":1},{"id":"first_name_label","component":"Text","text":"First Name","variant":"caption"},{"id":"first_name_field","component":"TextField","label":"First Name","value":{"path":"/contact/firstName"},"variant":"shortText"},{"id":"last_name_group","component":"Column","children":["last_name_label","last_name_field"],"weight":1},{"id":"last_name_label","component":"Text","text":"Last Name","variant":"caption"},{"id":"last_name_field","component":"TextField","label":"Last Name","value":{"path":"/contact/lastName"},"variant":"shortText"},{"id":"email_group","component":"Column","children":["email_label","email_field"]},{"id":"email_label","component":"Text","text":"Email Address","variant":"caption"},{"id":"email_field","component":"TextField","label":"Email","value":{"path":"/contact/email"},"variant":"shortText","checks":[{"call":"required","message":"Email is required."},{"call":"email","message":"Please enter a valid email address."}]},{"id":"phone_group","component":"Column","children":["phone_label","phone_field"]},{"id":"phone_label","component":"Text","text":"Phone Number","variant":"caption"},{"id":"phone_field","component":"TextField","label":"Phone","value":{"path":"/contact/phone"},"variant":"shortText","checks":[{"call":"regex","args":{"pattern":"^\\d{10}$"},"message":"Phone number must be 10 digits."}]},{"id":"pref_group","component":"Column","children":["pref_label","pref_picker"]},{"id":"pref_label","component":"Text","text":"Preferred Contact Method","variant":"caption"},{"id":"pref_picker","component":"ChoicePicker","variant":"mutuallyExclusive","options":[{"label":"Email","value":"email"},{"label":"Phone","value":"phone"},{"label":"SMS","value":"sms"}],"value":{"path":"/contact/preference"}},{"id":"divider_1","component":"Divider","axis":"horizontal"},{"id":"newsletter_checkbox","component":"CheckBox","label":"Subscribe to our newsletter","value":{"path":"/contact/subscribe"}},{"id":"submit_button_label","component":"Text","text":"Send Message"},{"id":"submit_button","component":"Button","child":"submit_button_label","variant":"primary","action":{"name":"submitContactForm","context":{"formId":"contact_form_1","clientTime":{"call":"now","returnType":"string"},"isNewsletterSubscribed":{"path":"/contact/subscribe"}}}}]}} {"updateDataModel":{"surfaceId":"contact_form_1","path":"/contact","value":{"firstName":"John","lastName":"Doe","email":"john.doe@example.com","phone":"1234567890","preference":["email"],"subscribe":true}}} {"deleteSurface":{"surfaceId":"contact_form_1"}} \ No newline at end of file diff --git a/specification/v0_9/test/cases/contact_form_example_test.json b/specification/v0_9/test/cases/contact_form_example_test.json index 69a86b37c..129ba7087 100644 --- a/specification/v0_9/test/cases/contact_form_example_test.json +++ b/specification/v0_9/test/cases/contact_form_example_test.json @@ -69,7 +69,6 @@ "checks": [ { "call": "email", - "args": [{ "path": "/contact/email" }], "message": "Please enter a valid email address." } ] From c1628f7376e6d5b0a289b44b673537a146ecd0c8 Mon Sep 17 00:00:00 2001 From: Greg Spencer Date: Wed, 28 Jan 2026 14:34:53 -0800 Subject: [PATCH 2/7] Revert to named parameters for function calling. Reverts #515 --- specification/v0_9/docs/a2ui_protocol.md | 11 +- specification/v0_9/docs/evolution_guide.md | 2 +- specification/v0_9/json/standard_catalog.json | 162 ++++++++++-------- 3 files changed, 98 insertions(+), 77 deletions(-) diff --git a/specification/v0_9/docs/a2ui_protocol.md b/specification/v0_9/docs/a2ui_protocol.md index b4ffba63b..20057210f 100644 --- a/specification/v0_9/docs/a2ui_protocol.md +++ b/specification/v0_9/docs/a2ui_protocol.md @@ -719,17 +719,10 @@ Values from the data model can be interpolated using their JSON Pointer path. "id": "user_welcome", "component": "Text", "text": { -<<<<<<< HEAD "call": "formatString", - "args": [ - "Hello, ${/user/firstName}! Welcome back to ${/appName}." - ] -======= - "call": "string_format", "args": { "value": "Hello, ${/user/firstName}! Welcome back to ${/appName}." } ->>>>>>> parent of d11883f (feat(protocol)!: refactor FunctionCall args to positional array (#515)) } } ``` @@ -739,7 +732,7 @@ Values from the data model can be interpolated using their JSON Pointer path. Results of client-side functions can be interpolated. Function calls are identified by the presence of parentheses `()`. - `${now()}`: A function with no arguments. -- `${formatDate(${/currentDate}, 'yyyy-MM-dd')}`: A function with positional arguments. +- `${formatDate(value:${/currentDate}, format:'yyyy-MM-dd')}`: A function with named arguments. Arguments can be **Literals** (quoted strings, numbers, or booleans), or **Nested Expressions**. @@ -747,7 +740,7 @@ Arguments can be **Literals** (quoted strings, numbers, or booleans), or **Neste Expressions can be nested using additional `${...}` wrappers inside an outer expression to make bindings explicit or to chain function calls. -- **Explicit Binding**: `${formatDate(${/currentDate}, 'yyyy-MM-dd')}` +- **Explicit Binding**: `${formatDate(value:${/currentDate}, format:'yyyy-MM-dd')}` - **Nested Functions**: `${upper(${now()})}` #### `formatString` type conversion diff --git a/specification/v0_9/docs/evolution_guide.md b/specification/v0_9/docs/evolution_guide.md index 8a4c7dcf8..fb94fc123 100644 --- a/specification/v0_9/docs/evolution_guide.md +++ b/specification/v0_9/docs/evolution_guide.md @@ -228,7 +228,7 @@ Specifying an unknown surfaceId will cause an error. It is recommended that clie - **String Formatting**: Introduced the `formatString` function, which supports `${expression}` syntax for interpolation. - **Unified Expression Language**: Allows embedding JSON Pointer paths (absolute and relative) and client-side function calls directly within the format string. -- **Nesting**: Supports recursive nesting of expressions (e.g., `${formatDate(${/timestamp}, 'yyyy-MM-dd')}`). +- **Nesting**: Supports recursive nesting of expressions (e.g., `${formatDate(value:${/timestamp}, format:'yyyy-MM-dd')}`). - **Restriction**: String interpolation `${...}` is **ONLY** supported within the `formatString` function. It is not supported in general for string properties, in order to strictly separate data binding definitions from static content. - **Reason**: Improves readability for complex strings. Instead of generating complex nested JSON objects (like chained concatenations) to combine strings and data, the model can write natural-looking template literals within the `formatString` function. diff --git a/specification/v0_9/json/standard_catalog.json b/specification/v0_9/json/standard_catalog.json index 76f2b8992..593711edd 100644 --- a/specification/v0_9/json/standard_catalog.json +++ b/specification/v0_9/json/standard_catalog.json @@ -753,7 +753,7 @@ }, { "name": "formatString", - "description": "Performs string interpolation of data model values and other functions in the catalog functions list and returns the resulting string. The value string can contain interpolated expressions in the `${expression}` format. Supported expression types include: JSON Pointer paths to the data model (e.g., `${/absolute/path}` or `${relative/path}`), and client-side function calls (e.g., `${now()}`). Function arguments must be literals (quoted strings, numbers, booleans) or nested expressions (e.g., `${formatDate(${/currentDate}, 'MM-dd')}`). To include a literal `${` sequence, escape it as `\\${`.", + "description": "Performs string interpolation of data model values and other functions in the catalog functions list and returns the resulting string. The value string can contain interpolated expressions in the `${expression}` format. Supported expression types include: JSON Pointer paths to the data model (e.g., `${/absolute/path}` or `${relative/path}`), and client-side function calls (e.g., `${now()}`). Function arguments must be named (e.g., `${formatDate(value:${/currentDate}, format:'MM-dd')}`). To include a literal `${` sequence, escape it as `\\${`.", "returnType": "string", "parameters": { "allOf": [ @@ -773,22 +773,27 @@ "description": "Formats a number with the specified grouping and decimal precision.", "returnType": "string", "parameters": { - "type": "array", - "items": [ - { - "$ref": "common_types.json#/$defs/DynamicNumber", - "description": "The number to format." - }, - { - "$ref": "common_types.json#/$defs/DynamicNumber", - "description": "Optional. The number of decimal places to show. Defaults to 0 or 2 depending on locale." - }, + "allOf": [ + { "$ref": "#/$defs/valueParam" }, { - "$ref": "common_types.json#/$defs/DynamicBoolean", - "description": "Optional. If true, uses locale-specific grouping separators (e.g. '1,000'). If false, returns raw digits (e.g. '1000'). Defaults to true." + "type": "object", + "properties": { + "value": { + "$ref": "common_types.json#/$defs/DynamicNumber", + "description": "The number to format." + }, + "decimals": { + "$ref": "common_types.json#/$defs/DynamicNumber", + "description": "Optional. The number of decimal places to show. Defaults to 0 or 2 depending on locale." + }, + "grouping": { + "$ref": "common_types.json#/$defs/DynamicBoolean", + "description": "Optional. If true, uses locale-specific grouping separators (e.g. '1,000'). If false, returns raw digits (e.g. '1000'). Defaults to true." + } + } } ], - "minItems": 1 + "unevaluatedProperties": false } }, { @@ -796,18 +801,32 @@ "description": "Formats a number as a currency string.", "returnType": "string", "parameters": { - "type": "array", - "items": [ - { - "$ref": "common_types.json#/$defs/DynamicNumber", - "description": "The monetary amount." - }, + "allOf": [ + { "$ref": "#/$defs/valueParam" }, { - "$ref": "common_types.json#/$defs/DynamicString", - "description": "The ISO 4217 currency code (e.g., 'USD', 'EUR')." + "type": "object", + "properties": { + "value": { + "$ref": "common_types.json#/$defs/DynamicNumber", + "description": "The monetary amount." + }, + "currency": { + "$ref": "common_types.json#/$defs/DynamicString", + "description": "The ISO 4217 currency code (e.g., 'USD', 'EUR')." + }, + "decimals": { + "$ref": "common_types.json#/$defs/DynamicNumber", + "description": "Optional. The number of decimal places to show. Defaults to 0 or 2 depending on locale." + }, + "grouping": { + "$ref": "common_types.json#/$defs/DynamicBoolean", + "description": "Optional. If true, uses locale-specific grouping separators (e.g. '1,000'). If false, returns raw digits (e.g. '1000'). Defaults to true." + } + }, + "required": ["currency"] } ], - "minItems": 2 + "unevaluatedProperties": false } }, { @@ -815,18 +834,24 @@ "description": "Formats a timestamp into a string using a pattern.", "returnType": "string", "parameters": { - "type": "array", - "items": [ - { - "$ref": "common_types.json#/$defs/DynamicValue", - "description": "The date to format." - }, + "allOf": [ + { "$ref": "#/$defs/valueParam" }, { - "$ref": "common_types.json#/$defs/DynamicString", - "description": "A Unicode TR35 date pattern string.\n\nToken Reference:\n- Year: 'yy' (26), 'yyyy' (2026)\n- Month: 'M' (1), 'MM' (01), 'MMM' (Jan), 'MMMM' (January)\n- Day: 'd' (1), 'dd' (01), 'E' (Tue), 'EEEE' (Tuesday)\n- Hour (12h): 'h' (1-12), 'hh' (01-12) - requires 'a' for AM/PM\n- Hour (24h): 'H' (0-23), 'HH' (00-23) - Military Time\n- Minute: 'mm' (00-59)\n- Second: 'ss' (00-59)\n- Period: 'a' (AM/PM)\n\nExamples:\n- 'MMM dd, yyyy' -> 'Jan 16, 2026'\n- 'HH:mm' -> '14:30' (Military)\n- 'h:mm a' -> '2:30 PM'\n- 'EEEE, d MMMM' -> 'Friday, 16 January'" + "type": "object", + "properties": { + "value": { + "$ref": "common_types.json#/$defs/DynamicValue", + "description": "The date to format." + }, + "format": { + "$ref": "common_types.json#/$defs/DynamicString", + "description": "A Unicode TR35 date pattern string.\n\nToken Reference:\n- Year: 'yy' (26), 'yyyy' (2026)\n- Month: 'M' (1), 'MM' (01), 'MMM' (Jan), 'MMMM' (January)\n- Day: 'd' (1), 'dd' (01), 'E' (Tue), 'EEEE' (Tuesday)\n- Hour (12h): 'h' (1-12), 'hh' (01-12) - requires 'a' for AM/PM\n- Hour (24h): 'H' (0-23), 'HH' (00-23) - Military Time\n- Minute: 'mm' (00-59)\n- Second: 'ss' (00-59)\n- Period: 'a' (AM/PM)\n\nExamples:\n- 'MMM dd, yyyy' -> 'Jan 16, 2026'\n- 'HH:mm' -> '14:30' (Military)\n- 'h:mm a' -> '2:30 PM'\n- 'EEEE, d MMMM' -> 'Friday, 16 January'" + } + }, + "required": ["format"] } ], - "minItems": 2 + "unevaluatedProperties": false } }, { @@ -834,49 +859,52 @@ "description": "Returns a localized string based on the Common Locale Data Repository (CLDR) plural category of the count (zero, one, two, few, many, other). Requires an 'other' fallback. For English, just use 'one' and 'other'.", "returnType": "string", "parameters": { - "type": "array", - "items": [ - { - "$ref": "common_types.json#/$defs/DynamicNumber", - "description": "The numeric value used to determine the plural category." - }, + "allOf": [ + { "$ref": "#/$defs/valueParam" }, { "type": "object", - "description": "A map of CLDR plural categories to their corresponding strings.", "properties": { - "zero": { - "$ref": "common_types.json#/$defs/DynamicString", - "description": "String for the 'zero' category (e.g., 0 items)." - }, - "one": { - "$ref": "common_types.json#/$defs/DynamicString", - "description": "String for the 'one' category (e.g., 1 item)." - }, - "two": { - "$ref": "common_types.json#/$defs/DynamicString", - "description": "String for the 'two' category (used in Arabic, Welsh, etc.)." + "value": { + "$ref": "common_types.json#/$defs/DynamicNumber", + "description": "The numeric value used to determine the plural category." }, - "few": { - "$ref": "common_types.json#/$defs/DynamicString", - "description": "String for the 'few' category (e.g., small groups in Slavic languages)." - }, - "many": { - "$ref": "common_types.json#/$defs/DynamicString", - "description": "String for the 'many' category (e.g., large groups in various languages)." - }, - "other": { - "$ref": "common_types.json#/$defs/DynamicString", - "description": "The default/fallback string (used for general plural cases)." + "forms": { + "type": "object", + "description": "A map of CLDR plural categories to their corresponding strings.", + "properties": { + "zero": { + "$ref": "common_types.json#/$defs/DynamicString", + "description": "String for the 'zero' category (e.g., 0 items)." + }, + "one": { + "$ref": "common_types.json#/$defs/DynamicString", + "description": "String for the 'one' category (e.g., 1 item)." + }, + "two": { + "$ref": "common_types.json#/$defs/DynamicString", + "description": "String for the 'two' category (used in Arabic, Welsh, etc.)." + }, + "few": { + "$ref": "common_types.json#/$defs/DynamicString", + "description": "String for the 'few' category (e.g., small groups in Slavic languages)." + }, + "many": { + "$ref": "common_types.json#/$defs/DynamicString", + "description": "String for the 'many' category (e.g., large groups in various languages)." + }, + "other": { + "$ref": "common_types.json#/$defs/DynamicString", + "description": "The default/fallback string (used for general plural cases)." + } + }, + "required": ["other"], + "additionalProperties": false } }, - "required": [ - "other" - ], - "additionalProperties": false + "required": ["forms"] } ], - "minItems": 2, - "maxItems": 2 + "unevaluatedProperties": false } }, { From 938cdaf1c66bd1c09a15ef4379264c13967b7c3b Mon Sep 17 00:00:00 2001 From: Greg Spencer Date: Wed, 28 Jan 2026 14:50:10 -0800 Subject: [PATCH 3/7] Fix tests --- specification/v0_9/docs/a2ui_protocol.md | 4 +- .../v0_9/test/cases/checkable_components.json | 50 +++++++++++++++---- .../test/cases/contact_form_example.jsonl | 2 +- 3 files changed, 44 insertions(+), 12 deletions(-) diff --git a/specification/v0_9/docs/a2ui_protocol.md b/specification/v0_9/docs/a2ui_protocol.md index 20057210f..86cc5118f 100644 --- a/specification/v0_9/docs/a2ui_protocol.md +++ b/specification/v0_9/docs/a2ui_protocol.md @@ -282,8 +282,8 @@ The following example demonstrates a complete interaction to render a Contact Fo ```jsonl {"createSurface":{"surfaceId":"contact_form_1","catalogId":"https://a2ui.dev/specification/v0_9/standard_catalog.json"}} -{"updateComponents":{"surfaceId":"contact_form_1","components":[{"id":"root","component":"Card","child":"form_container"},{"id":"form_container","component":"Column","children":["header_row","name_row","email_group","phone_group","pref_group","divider_1","newsletter_checkbox","submit_button"],"justify":"start","align":"stretch"},{"id":"header_row","component":"Row","children":["header_icon","header_text"],"align":"center"},{"id":"header_icon","component":"Icon","name":"mail"},{"id":"header_text","component":"Text","text":"# Contact Us","variant":"h2"},{"id":"name_row","component":"Row","children":["first_name_group","last_name_group"],"justify":"spaceBetween"},{"id":"first_name_group","component":"Column","children":["first_name_label","first_name_field"],"weight":1},{"id":"first_name_label","component":"Text","text":"First Name","variant":"caption"},{"id":"first_name_field","component":"TextField","label":"First Name","value":{"path":"/contact/firstName"},"variant":"shortText"},{"id":"last_name_group","component":"Column","children":["last_name_label","last_name_field"],"weight":1},{"id":"last_name_label","component":"Text","text":"Last Name","variant":"caption"},{"id":"last_name_field","component":"TextField","label":"Last Name","value":{"path":"/contact/lastName"},"variant":"shortText"},{"id":"email_group","component":"Column","children":["email_label","email_field"]},{"id":"email_label","component":"Text","text":"Email Address","variant":"caption"},{"id":"email_field","component":"TextField","label":"Email","value":{"path":"/contact/email"},"variant":"shortText","checks":[{"call":"required","message":"Email is required."},{"call":"email","message":"Please enter a valid email address."}]},{"id":"phone_group","component":"Column","children":["phone_label","phone_field"]},{"id":"phone_label","component":"Text","text":"Phone Number","variant":"caption"},{"id":"phone_field","component":"TextField","label":"Phone","value":{"path":"/contact/phone"},"variant":"shortText","checks":[{"call":"regex","args":{"pattern":"^\\d{10}$"},"message":"Phone number must be 10 digits."}]},{"id":"pref_group","component":"Column","children":["pref_label","pref_picker"]},{"id":"pref_label","component":"Text","text":"Preferred Contact Method","variant":"caption"},{"id":"pref_picker","component":"ChoicePicker","variant":"mutuallyExclusive","options":[{"label":"Email","value":"email"},{"label":"Phone","value":"phone"},{"label":"SMS","value":"sms"}],"value":{"path":"/contact/preference"}},{"id":"divider_1","component":"Divider","axis":"horizontal"},{"id":"newsletter_checkbox","component":"CheckBox","label":"Subscribe to our newsletter","value":{"path":"/contact/subscribe"}},{"id":"submit_button_label","component":"Text","text":"Send Message"},{"id":"submit_button","component":"Button","child":"submit_button_label","variant":"primary","action":{"name":"submitContactForm","context":{"formId":"contact_form_1","clientTime":{"call":"now","returnType":"string"},"isNewsletterSubscribed":{"path":"/contact/subscribe"}}}}]}} -{"updateDataModel":{"surfaceId":"contact_form_1","path":"/contact","value":{"firstName":"John","lastName":"Doe"}}} +{"updateComponents":{"surfaceId":"contact_form_1","components":[{"id":"root","component":"Card","child":"form_container"},{"id":"form_container","component":"Column","children":["header_row","name_row","email_group","phone_group","pref_group","divider_1","newsletter_checkbox","submit_button"],"justify":"start","align":"stretch"},{"id":"header_row","component":"Row","children":["header_icon","header_text"],"align":"center"},{"id":"header_icon","component":"Icon","name":"mail"},{"id":"header_text","component":"Text","text":"# Contact Us","variant":"h2"},{"id":"name_row","component":"Row","children":["first_name_group","last_name_group"],"justify":"spaceBetween"},{"id":"first_name_group","component":"Column","children":["first_name_label","first_name_field"],"weight":1},{"id":"first_name_label","component":"Text","text":"First Name","variant":"caption"},{"id":"first_name_field","component":"TextField","label":"First Name","value":{"path":"/contact/firstName"},"variant":"shortText"},{"id":"last_name_group","component":"Column","children":["last_name_label","last_name_field"],"weight":1},{"id":"last_name_label","component":"Text","text":"Last Name","variant":"caption"},{"id":"last_name_field","component":"TextField","label":"Last Name","value":{"path":"/contact/lastName"},"variant":"shortText"},{"id":"email_group","component":"Column","children":["email_label","email_field"]},{"id":"email_label","component":"Text","text":"Email Address","variant":"caption"},{"id":"email_field","component":"TextField","label":"Email","value":{"path":"/contact/email"},"variant":"shortText","checks":[{"call":"required","args":{"value":{"path":"/contact/email"}},"message":"Email is required."},{"call":"email","args":{"value":{"path":"/contact/email"}},"message":"Please enter a valid email address."}]},{"id":"phone_group","component":"Column","children":["phone_label","phone_field"]},{"id":"phone_label","component":"Text","text":"Phone Number","variant":"caption"},{"id":"phone_field","component":"TextField","label":"Phone","value":{"path":"/contact/phone"},"variant":"shortText","checks":[{"call":"regex","args":{"value":{"path":"/contact/phone"},"pattern":"^\\d{10}$"},"message":"Phone number must be 10 digits."}]},{"id":"pref_group","component":"Column","children":["pref_label","pref_picker"]},{"id":"pref_label","component":"Text","text":"Preferred Contact Method","variant":"caption"},{"id":"pref_picker","component":"ChoicePicker","variant":"mutuallyExclusive","options":[{"label":"Email","value":"email"},{"label":"Phone","value":"phone"},{"label":"SMS","value":"sms"}],"value":{"path":"/contact/preference"}},{"id":"divider_1","component":"Divider","axis":"horizontal"},{"id":"newsletter_checkbox","component":"CheckBox","label":"Subscribe to our newsletter","value":{"path":"/contact/subscribe"}},{"id":"submit_button_label","component":"Text","text":"Send Message"},{"id":"submit_button","component":"Button","child":"submit_button_label","variant":"primary","action":{"event":{"name":"submitContactForm","context":{"formId":"contact_form_1","clientTime":{"call":"now","args":{},"returnType":"string"},"isNewsletterSubscribed":{"path":"/contact/subscribe"}}}}}]}} +{"updateDataModel":{"surfaceId":"contact_form_1","path":"/contact","value":{"firstName":"John","lastName":"Doe","email":"john.doe@example.com","phone":"1234567890","preference":["email"],"subscribe":true}}} {"deleteSurface":{"surfaceId":"contact_form_1"}} ``` diff --git a/specification/v0_9/test/cases/checkable_components.json b/specification/v0_9/test/cases/checkable_components.json index 9a0f1a93d..ade580f29 100644 --- a/specification/v0_9/test/cases/checkable_components.json +++ b/specification/v0_9/test/cases/checkable_components.json @@ -160,7 +160,10 @@ "checks": [ { "call": "regex", - "args": [{ "path": "/formData/phone" }, "^\\d{10}$"], + "args": { + "value": { "path": "/formData/phone" }, + "pattern": "^\\d{10}$" + }, "message": "Must be 10 digits" } ] @@ -184,7 +187,11 @@ "checks": [ { "call": "length", - "args": [{ "path": "/formData/pw" }, { "min": 8, "max": 64 }], + "args": { + "value": { "path": "/formData/pw" }, + "min": 8, + "max": 64 + }, "message": "Password must be 8-64 characters" } ] @@ -210,7 +217,11 @@ "checks": [ { "call": "numeric", - "args": [{ "path": "/formData/score" }, { "min": 0, "max": 100 }], + "args": { + "value": { "path": "/formData/score" }, + "min": 0, + "max": 100 + }, "message": "Score must be between 0 and 100" } ] @@ -234,11 +245,28 @@ "checks": [ { "and": [ - { "call": "required", "args": [{ "path": "/formData/code" }] }, + { + "call": "required", + "args": { "value": { "path": "/formData/code" } } + }, { "or": [ - { "call": "regex", "args": [{ "path": "/formData/code" }, "^[A-Z]"] }, - { "not": { "call": "regex", "args": [{ "path": "/formData/code" }, "^[0-9]"] } } + { + "call": "regex", + "args": { + "value": { "path": "/formData/code" }, + "pattern": "^[A-Z]" + } + }, + { + "not": { + "call": "regex", + "args": { + "value": { "path": "/formData/code" }, + "pattern": "^[0-9]" + } + } + } ] } ], @@ -265,7 +293,7 @@ "checks": [ { "call": "email", - "args": [{ "path": "/formData/email" }] + "args": { "value": { "path": "/formData/email" } } } ] } @@ -288,7 +316,7 @@ "checks": [ { "call": "formatString", - "args": ["Hello ${/formData/name}"], + "args": { "value": "Hello ${/formData/name}" }, "returnType": "string", "message": "This should fail because returnType is string, not boolean" } @@ -319,7 +347,11 @@ "checks": [ { "call": "length", - "args": [{ "path": "/formData/interests" }, { "min": 2, "max": 2 }], + "args": { + "value": { "path": "/formData/interests" }, + "min": 2, + "max": 2 + }, "message": "Select exactly 2 interests" } ] diff --git a/specification/v0_9/test/cases/contact_form_example.jsonl b/specification/v0_9/test/cases/contact_form_example.jsonl index 43a1c6dca..52549e983 100644 --- a/specification/v0_9/test/cases/contact_form_example.jsonl +++ b/specification/v0_9/test/cases/contact_form_example.jsonl @@ -1,4 +1,4 @@ {"createSurface":{"surfaceId":"contact_form_1","catalogId":"https://a2ui.dev/specification/v0_9/standard_catalog.json"}} -{"updateComponents":{"surfaceId":"contact_form_1","components":[{"id":"root","component":"Card","child":"form_container"},{"id":"form_container","component":"Column","children":["header_row","name_row","email_group","phone_group","pref_group","divider_1","newsletter_checkbox","submit_button"],"justify":"start","align":"stretch"},{"id":"header_row","component":"Row","children":["header_icon","header_text"],"align":"center"},{"id":"header_icon","component":"Icon","name":"mail"},{"id":"header_text","component":"Text","text":"# Contact Us","variant":"h2"},{"id":"name_row","component":"Row","children":["first_name_group","last_name_group"],"justify":"spaceBetween"},{"id":"first_name_group","component":"Column","children":["first_name_label","first_name_field"],"weight":1},{"id":"first_name_label","component":"Text","text":"First Name","variant":"caption"},{"id":"first_name_field","component":"TextField","label":"First Name","value":{"path":"/contact/firstName"},"variant":"shortText"},{"id":"last_name_group","component":"Column","children":["last_name_label","last_name_field"],"weight":1},{"id":"last_name_label","component":"Text","text":"Last Name","variant":"caption"},{"id":"last_name_field","component":"TextField","label":"Last Name","value":{"path":"/contact/lastName"},"variant":"shortText"},{"id":"email_group","component":"Column","children":["email_label","email_field"]},{"id":"email_label","component":"Text","text":"Email Address","variant":"caption"},{"id":"email_field","component":"TextField","label":"Email","value":{"path":"/contact/email"},"variant":"shortText","checks":[{"call":"required","message":"Email is required."},{"call":"email","message":"Please enter a valid email address."}]},{"id":"phone_group","component":"Column","children":["phone_label","phone_field"]},{"id":"phone_label","component":"Text","text":"Phone Number","variant":"caption"},{"id":"phone_field","component":"TextField","label":"Phone","value":{"path":"/contact/phone"},"variant":"shortText","checks":[{"call":"regex","args":{"pattern":"^\\d{10}$"},"message":"Phone number must be 10 digits."}]},{"id":"pref_group","component":"Column","children":["pref_label","pref_picker"]},{"id":"pref_label","component":"Text","text":"Preferred Contact Method","variant":"caption"},{"id":"pref_picker","component":"ChoicePicker","variant":"mutuallyExclusive","options":[{"label":"Email","value":"email"},{"label":"Phone","value":"phone"},{"label":"SMS","value":"sms"}],"value":{"path":"/contact/preference"}},{"id":"divider_1","component":"Divider","axis":"horizontal"},{"id":"newsletter_checkbox","component":"CheckBox","label":"Subscribe to our newsletter","value":{"path":"/contact/subscribe"}},{"id":"submit_button_label","component":"Text","text":"Send Message"},{"id":"submit_button","component":"Button","child":"submit_button_label","variant":"primary","action":{"name":"submitContactForm","context":{"formId":"contact_form_1","clientTime":{"call":"now","returnType":"string"},"isNewsletterSubscribed":{"path":"/contact/subscribe"}}}}]}} +{"updateComponents":{"surfaceId":"contact_form_1","components":[{"id":"root","component":"Card","child":"form_container"},{"id":"form_container","component":"Column","children":["header_row","name_row","email_group","phone_group","pref_group","divider_1","newsletter_checkbox","submit_button"],"justify":"start","align":"stretch"},{"id":"header_row","component":"Row","children":["header_icon","header_text"],"align":"center"},{"id":"header_icon","component":"Icon","name":"mail"},{"id":"header_text","component":"Text","text":"# Contact Us","variant":"h2"},{"id":"name_row","component":"Row","children":["first_name_group","last_name_group"],"justify":"spaceBetween"},{"id":"first_name_group","component":"Column","children":["first_name_label","first_name_field"],"weight":1},{"id":"first_name_label","component":"Text","text":"First Name","variant":"caption"},{"id":"first_name_field","component":"TextField","label":"First Name","value":{"path":"/contact/firstName"},"variant":"shortText"},{"id":"last_name_group","component":"Column","children":["last_name_label","last_name_field"],"weight":1},{"id":"last_name_label","component":"Text","text":"Last Name","variant":"caption"},{"id":"last_name_field","component":"TextField","label":"Last Name","value":{"path":"/contact/lastName"},"variant":"shortText"},{"id":"email_group","component":"Column","children":["email_label","email_field"]},{"id":"email_label","component":"Text","text":"Email Address","variant":"caption"},{"id":"email_field","component":"TextField","label":"Email","value":{"path":"/contact/email"},"variant":"shortText","checks":[{"call":"required","args":{"value":{"path":"/contact/email"}},"message":"Email is required."},{"call":"email","args":{"value":{"path":"/contact/email"}},"message":"Please enter a valid email address."}]},{"id":"phone_group","component":"Column","children":["phone_label","phone_field"]},{"id":"phone_label","component":"Text","text":"Phone Number","variant":"caption"},{"id":"phone_field","component":"TextField","label":"Phone","value":{"path":"/contact/phone"},"variant":"shortText","checks":[{"call":"regex","args":{"value":{"path":"/contact/phone"},"pattern":"^\\d{10}$"},"message":"Phone number must be 10 digits."}]},{"id":"pref_group","component":"Column","children":["pref_label","pref_picker"]},{"id":"pref_label","component":"Text","text":"Preferred Contact Method","variant":"caption"},{"id":"pref_picker","component":"ChoicePicker","variant":"mutuallyExclusive","options":[{"label":"Email","value":"email"},{"label":"Phone","value":"phone"},{"label":"SMS","value":"sms"}],"value":{"path":"/contact/preference"}},{"id":"divider_1","component":"Divider","axis":"horizontal"},{"id":"newsletter_checkbox","component":"CheckBox","label":"Subscribe to our newsletter","value":{"path":"/contact/subscribe"}},{"id":"submit_button_label","component":"Text","text":"Send Message"},{"id":"submit_button","component":"Button","child":"submit_button_label","variant":"primary","action":{"event":{"name":"submitContactForm","context":{"formId":"contact_form_1","clientTime":{"call":"now","args":{},"returnType":"string"},"isNewsletterSubscribed":{"path":"/contact/subscribe"}}}}}]}} {"updateDataModel":{"surfaceId":"contact_form_1","path":"/contact","value":{"firstName":"John","lastName":"Doe","email":"john.doe@example.com","phone":"1234567890","preference":["email"],"subscribe":true}}} {"deleteSurface":{"surfaceId":"contact_form_1"}} \ No newline at end of file From 1ce76d2a60cfd272a81c95fcecb62d0c89bd61f0 Mon Sep 17 00:00:00 2001 From: Greg Spencer Date: Wed, 28 Jan 2026 15:11:14 -0800 Subject: [PATCH 4/7] Fix evals. --- specification/v0_9/docs/a2ui_protocol.md | 7 ------- specification/v0_9/eval/src/index.ts | 16 ++++++++++++++++ 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/specification/v0_9/docs/a2ui_protocol.md b/specification/v0_9/docs/a2ui_protocol.md index 86cc5118f..69d6c2d5d 100644 --- a/specification/v0_9/docs/a2ui_protocol.md +++ b/specification/v0_9/docs/a2ui_protocol.md @@ -568,17 +568,10 @@ _Replace the entire data model:_ When `sendDataModel` is set to `true` for a surface, the client automatically appends the **entire data model** of that surface to the metadata of every message (such as `action` or user query) sent to the server that created the surface. The data model is included using the transport's metadata facility (e.g., the `metadata` field in A2A or a header in HTTP). The payload follows the schema in [`a2ui_client_data_model.json`](../json/a2ui_client_data_model.json). -<<<<<<< HEAD - -- **Targeted Delivery**: The data model is sent exclusively to the server that created the surface. Data cannot leak to other agents or servers. -- **Trigger**: Data is sent only when a client-to-server message is triggered (e.g., by a user action like a button click). Passive data changes (like typing in a text field) do not trigger a network request on their own; they simply update the local state, which will be sent with the next action. -- **Payload**: The data model is included in the transport metadata, tagged by its `surfaceId`. -- # **Convergence**: The server treats the received data model as the current state of the client at the time of the action. - **Targeted Delivery**: The data model is sent exclusively to the server that created the surface. Data cannot leak to other agents or servers. - **Trigger:** Data is sent only when an A2A message is triggered (e.g., by a user action like a button click). Passive data changes (like typing in a text field) do not trigger a network request on their own; they simply update the local state, which will be sent with the next action. - **Payload:** The data model is included in the A2A message metadata, tagged by its `surfaceId`. - **Convergence:** The server treats the received data model as the current state of the client at the time of the action. - > > > > > > > parent of d11883f (feat(protocol)!: refactor FunctionCall args to positional array (#515)) ## Client-side logic & validation diff --git a/specification/v0_9/eval/src/index.ts b/specification/v0_9/eval/src/index.ts index 2715e4d61..7c10556ed 100644 --- a/specification/v0_9/eval/src/index.ts +++ b/specification/v0_9/eval/src/index.ts @@ -40,6 +40,22 @@ function loadSchemas(): Record { const schema = JSON.parse(schemaString); schemas[path.basename(file)] = schema; } + + // Alias standard_catalog.json to catalog.json to match server_to_client.json references + // This mirrors the logic in run_tests.py + if (schemas["standard_catalog.json"]) { + const catalogSchema = JSON.parse( + JSON.stringify(schemas["standard_catalog.json"]), + ); + if (catalogSchema["$id"]) { + catalogSchema["$id"] = catalogSchema["$id"].replace( + "standard_catalog.json", + "catalog.json", + ); + } + schemas["catalog.json"] = catalogSchema; + } + return schemas; } From b1a92c57f364e47fdc496a3d6cad11552bf37a76 Mon Sep 17 00:00:00 2001 From: Greg Spencer Date: Wed, 28 Jan 2026 15:29:22 -0800 Subject: [PATCH 5/7] Restore DynamicValue --- specification/v0_9/docs/a2ui_protocol.md | 22 +- specification/v0_9/json/common_types.json | 76 ++--- specification/v0_9/json/standard_catalog.json | 313 ++++++++---------- .../test/cases/contact_form_example_test.json | 5 +- 4 files changed, 181 insertions(+), 235 deletions(-) diff --git a/specification/v0_9/docs/a2ui_protocol.md b/specification/v0_9/docs/a2ui_protocol.md index 69d6c2d5d..30aecad62 100644 --- a/specification/v0_9/docs/a2ui_protocol.md +++ b/specification/v0_9/docs/a2ui_protocol.md @@ -664,15 +664,19 @@ The [`standard_catalog.json`] provides the baseline set of components and functi ### Functions -| Function | Description | -| :--------------- | :----------------------------------------------------------------------- | -| **required** | Checks that the value is not null, undefined, or empty. | -| **regex** | Checks that the value matches a regular expression string. | -| **length** | Checks string length constraints. | -| **numeric** | Checks numeric range constraints. | -| **email** | Checks that the value is a valid email address. | -| **formatString** | Does string interpolation of data model values and registered functions. | -| **openUrl** | Opens a URL in a browser. | +| Function | Description | +| :----------------- | :----------------------------------------------------------------------- | +| **required** | Checks that the value is not null, undefined, or empty. | +| **regex** | Checks that the value matches a regular expression string. | +| **length** | Checks string length constraints. | +| **numeric** | Checks numeric range constraints. | +| **email** | Checks that the value is a valid email address. | +| **formatString** | Does string interpolation of data model values and registered functions. | +| **formatNumber** | Formats a number with grouping and precision. | +| **formatCurrency** | Formats a number as a currency string. | +| **formatDate** | Formats a date/time using a pattern. | +| **pluralize** | Selects a localized string based on a numeric count. | +| **openUrl** | Opens a URL in a browser. | ### Theme diff --git a/specification/v0_9/json/common_types.json b/specification/v0_9/json/common_types.json index e1c7f7b22..ffb569ae5 100644 --- a/specification/v0_9/json/common_types.json +++ b/specification/v0_9/json/common_types.json @@ -60,6 +60,17 @@ } ] }, + "DataBinding": { + "type": "object", + "properties": { + "path": { + "type": "string", + "description": "A JSON Pointer path to a value in the data model." + } + }, + "required": ["path"], + "additionalProperties": false + }, "DynamicValue": { "description": "A value that can be a literal, a path, or a function call returning any type.", "oneOf": [ @@ -73,14 +84,7 @@ "type": "boolean" }, { - "type": "object", - "properties": { - "path": { - "type": "string" - } - }, - "required": ["path"], - "additionalProperties": false + "$ref": "#/$defs/DataBinding" }, { "$ref": "#/$defs/FunctionCall" @@ -94,14 +98,7 @@ "type": "string" }, { - "type": "object", - "properties": { - "path": { - "type": "string" - } - }, - "required": ["path"], - "additionalProperties": false + "$ref": "#/$defs/DataBinding" }, { "allOf": [ @@ -127,14 +124,7 @@ "type": "number" }, { - "type": "object", - "properties": { - "path": { - "type": "string" - } - }, - "required": ["path"], - "additionalProperties": false + "$ref": "#/$defs/DataBinding" }, { "allOf": [ @@ -160,14 +150,7 @@ "type": "boolean" }, { - "type": "object", - "properties": { - "path": { - "type": "string" - } - }, - "required": ["path"], - "additionalProperties": false + "$ref": "#/$defs/DataBinding" }, { "$ref": "#/$defs/LogicExpression" @@ -184,14 +167,7 @@ } }, { - "type": "object", - "properties": { - "path": { - "type": "string" - } - }, - "required": ["path"], - "additionalProperties": false + "$ref": "#/$defs/DataBinding" }, { "allOf": [ @@ -222,13 +198,29 @@ "type": "object", "description": "Arguments passed to the function.", "additionalProperties": { - "$ref": "#/$defs/DynamicValue" + "anyOf": [ + { + "$ref": "#/$defs/DynamicValue" + }, + { + "type": "object", + "description": "A literal object argument (e.g. configuration)." + } + ] } }, "returnType": { "type": "string", "description": "The expected return type of the function call.", - "enum": ["string", "number", "boolean", "array", "object", "any", "void"], + "enum": [ + "string", + "number", + "boolean", + "array", + "object", + "any", + "void" + ], "default": "boolean" } }, diff --git a/specification/v0_9/json/standard_catalog.json b/specification/v0_9/json/standard_catalog.json index 593711edd..13c877f72 100644 --- a/specification/v0_9/json/standard_catalog.json +++ b/specification/v0_9/json/standard_catalog.json @@ -654,7 +654,13 @@ "description": "Checks that the value is not null, undefined, or empty.", "returnType": "boolean", "parameters": { - "allOf": [{ "$ref": "#/$defs/valueParam" }], + "type": "object", + "properties": { + "value": { + "description": "The value to check." + } + }, + "required": ["value"], "unevaluatedProperties": false } }, @@ -663,20 +669,15 @@ "description": "Checks that the value matches a regular expression string.", "returnType": "boolean", "parameters": { - "allOf": [ - { "$ref": "#/$defs/valueParam" }, - { - "type": "object", - "properties": { - "value": { "type": "string" }, - "pattern": { - "type": "string", - "description": "The regex pattern to match against." - } - }, - "required": ["pattern"] + "type": "object", + "properties": { + "value": { "$ref": "common_types.json#/$defs/DynamicString" }, + "pattern": { + "type": "string", + "description": "The regex pattern to match against." } - ], + }, + "required": ["value", "pattern"], "unevaluatedProperties": false } }, @@ -685,26 +686,22 @@ "description": "Checks string length constraints.", "returnType": "boolean", "parameters": { - "allOf": [ - { "$ref": "#/$defs/valueParam" }, - { - "type": "object", - "properties": { - "value": { "type": "string" }, - "min": { - "type": "integer", - "minimum": 0, - "description": "The minimum allowed length." - }, - "max": { - "type": "integer", - "minimum": 0, - "description": "The maximum allowed length." - } - }, - "anyOf": [{ "required": ["min"] }, { "required": ["max"] }] + "type": "object", + "properties": { + "value": { "$ref": "common_types.json#/$defs/DynamicString" }, + "min": { + "type": "integer", + "minimum": 0, + "description": "The minimum allowed length." + }, + "max": { + "type": "integer", + "minimum": 0, + "description": "The maximum allowed length." } - ], + }, + "required": ["value"], + "anyOf": [{ "required": ["min"] }, { "required": ["max"] }], "unevaluatedProperties": false } }, @@ -713,24 +710,20 @@ "description": "Checks numeric range constraints.", "returnType": "boolean", "parameters": { - "allOf": [ - { "$ref": "#/$defs/valueParam" }, - { - "type": "object", - "properties": { - "value": { "type": "number" }, - "min": { - "type": "number", - "description": "The minimum allowed value." - }, - "max": { - "type": "number", - "description": "The maximum allowed value." - } - }, - "anyOf": [{ "required": ["min"] }, { "required": ["max"] }] + "type": "object", + "properties": { + "value": { "$ref": "common_types.json#/$defs/DynamicNumber" }, + "min": { + "type": "number", + "description": "The minimum allowed value." + }, + "max": { + "type": "number", + "description": "The maximum allowed value." } - ], + }, + "required": ["value"], + "anyOf": [{ "required": ["min"] }, { "required": ["max"] }], "unevaluatedProperties": false } }, @@ -739,15 +732,11 @@ "description": "Checks that the value is a valid email address.", "returnType": "boolean", "parameters": { - "allOf": [ - { "$ref": "#/$defs/valueParam" }, - { - "type": "object", - "properties": { - "value": { "type": "string" } - } - } - ], + "type": "object", + "properties": { + "value": { "$ref": "common_types.json#/$defs/DynamicString" } + }, + "required": ["value"], "unevaluatedProperties": false } }, @@ -756,15 +745,11 @@ "description": "Performs string interpolation of data model values and other functions in the catalog functions list and returns the resulting string. The value string can contain interpolated expressions in the `${expression}` format. Supported expression types include: JSON Pointer paths to the data model (e.g., `${/absolute/path}` or `${relative/path}`), and client-side function calls (e.g., `${now()}`). Function arguments must be named (e.g., `${formatDate(value:${/currentDate}, format:'MM-dd')}`). To include a literal `${` sequence, escape it as `\\${`.", "returnType": "string", "parameters": { - "allOf": [ - { "$ref": "#/$defs/valueParam" }, - { - "type": "object", - "properties": { - "value": { "type": "string" } - } - } - ], + "type": "object", + "properties": { + "value": { "$ref": "common_types.json#/$defs/DynamicString" } + }, + "required": ["value"], "unevaluatedProperties": false } }, @@ -773,26 +758,22 @@ "description": "Formats a number with the specified grouping and decimal precision.", "returnType": "string", "parameters": { - "allOf": [ - { "$ref": "#/$defs/valueParam" }, - { - "type": "object", - "properties": { - "value": { - "$ref": "common_types.json#/$defs/DynamicNumber", - "description": "The number to format." - }, - "decimals": { - "$ref": "common_types.json#/$defs/DynamicNumber", - "description": "Optional. The number of decimal places to show. Defaults to 0 or 2 depending on locale." - }, - "grouping": { - "$ref": "common_types.json#/$defs/DynamicBoolean", - "description": "Optional. If true, uses locale-specific grouping separators (e.g. '1,000'). If false, returns raw digits (e.g. '1000'). Defaults to true." - } - } + "type": "object", + "properties": { + "value": { + "$ref": "common_types.json#/$defs/DynamicNumber", + "description": "The number to format." + }, + "decimals": { + "$ref": "common_types.json#/$defs/DynamicNumber", + "description": "Optional. The number of decimal places to show. Defaults to 0 or 2 depending on locale." + }, + "grouping": { + "$ref": "common_types.json#/$defs/DynamicBoolean", + "description": "Optional. If true, uses locale-specific grouping separators (e.g. '1,000'). If false, returns raw digits (e.g. '1000'). Defaults to true." } - ], + }, + "required": ["value"], "unevaluatedProperties": false } }, @@ -801,31 +782,26 @@ "description": "Formats a number as a currency string.", "returnType": "string", "parameters": { - "allOf": [ - { "$ref": "#/$defs/valueParam" }, - { - "type": "object", - "properties": { - "value": { - "$ref": "common_types.json#/$defs/DynamicNumber", - "description": "The monetary amount." - }, - "currency": { - "$ref": "common_types.json#/$defs/DynamicString", - "description": "The ISO 4217 currency code (e.g., 'USD', 'EUR')." - }, - "decimals": { - "$ref": "common_types.json#/$defs/DynamicNumber", - "description": "Optional. The number of decimal places to show. Defaults to 0 or 2 depending on locale." - }, - "grouping": { - "$ref": "common_types.json#/$defs/DynamicBoolean", - "description": "Optional. If true, uses locale-specific grouping separators (e.g. '1,000'). If false, returns raw digits (e.g. '1000'). Defaults to true." - } - }, - "required": ["currency"] + "type": "object", + "properties": { + "value": { + "$ref": "common_types.json#/$defs/DynamicNumber", + "description": "The monetary amount." + }, + "currency": { + "$ref": "common_types.json#/$defs/DynamicString", + "description": "The ISO 4217 currency code (e.g., 'USD', 'EUR')." + }, + "decimals": { + "$ref": "common_types.json#/$defs/DynamicNumber", + "description": "Optional. The number of decimal places to show. Defaults to 0 or 2 depending on locale." + }, + "grouping": { + "$ref": "common_types.json#/$defs/DynamicBoolean", + "description": "Optional. If true, uses locale-specific grouping separators (e.g. '1,000'). If false, returns raw digits (e.g. '1000'). Defaults to true." } - ], + }, + "required": ["currency", "value"], "unevaluatedProperties": false } }, @@ -834,23 +810,18 @@ "description": "Formats a timestamp into a string using a pattern.", "returnType": "string", "parameters": { - "allOf": [ - { "$ref": "#/$defs/valueParam" }, - { - "type": "object", - "properties": { - "value": { - "$ref": "common_types.json#/$defs/DynamicValue", - "description": "The date to format." - }, - "format": { - "$ref": "common_types.json#/$defs/DynamicString", - "description": "A Unicode TR35 date pattern string.\n\nToken Reference:\n- Year: 'yy' (26), 'yyyy' (2026)\n- Month: 'M' (1), 'MM' (01), 'MMM' (Jan), 'MMMM' (January)\n- Day: 'd' (1), 'dd' (01), 'E' (Tue), 'EEEE' (Tuesday)\n- Hour (12h): 'h' (1-12), 'hh' (01-12) - requires 'a' for AM/PM\n- Hour (24h): 'H' (0-23), 'HH' (00-23) - Military Time\n- Minute: 'mm' (00-59)\n- Second: 'ss' (00-59)\n- Period: 'a' (AM/PM)\n\nExamples:\n- 'MMM dd, yyyy' -> 'Jan 16, 2026'\n- 'HH:mm' -> '14:30' (Military)\n- 'h:mm a' -> '2:30 PM'\n- 'EEEE, d MMMM' -> 'Friday, 16 January'" - } - }, - "required": ["format"] + "type": "object", + "properties": { + "value": { + "$ref": "common_types.json#/$defs/DynamicValue", + "description": "The date to format." + }, + "format": { + "$ref": "common_types.json#/$defs/DynamicString", + "description": "A Unicode TR35 date pattern string.\n\nToken Reference:\n- Year: 'yy' (26), 'yyyy' (2026)\n- Month: 'M' (1), 'MM' (01), 'MMM' (Jan), 'MMMM' (January)\n- Day: 'd' (1), 'dd' (01), 'E' (Tue), 'EEEE' (Tuesday)\n- Hour (12h): 'h' (1-12), 'hh' (01-12) - requires 'a' for AM/PM\n- Hour (24h): 'H' (0-23), 'HH' (00-23) - Military Time\n- Minute: 'mm' (00-59)\n- Second: 'ss' (00-59)\n- Period: 'a' (AM/PM)\n\nExamples:\n- 'MMM dd, yyyy' -> 'Jan 16, 2026'\n- 'HH:mm' -> '14:30' (Military)\n- 'h:mm a' -> '2:30 PM'\n- 'EEEE, d MMMM' -> 'Friday, 16 January'" } - ], + }, + "required": ["format", "value"], "unevaluatedProperties": false } }, @@ -859,51 +830,38 @@ "description": "Returns a localized string based on the Common Locale Data Repository (CLDR) plural category of the count (zero, one, two, few, many, other). Requires an 'other' fallback. For English, just use 'one' and 'other'.", "returnType": "string", "parameters": { - "allOf": [ - { "$ref": "#/$defs/valueParam" }, - { - "type": "object", - "properties": { - "value": { - "$ref": "common_types.json#/$defs/DynamicNumber", - "description": "The numeric value used to determine the plural category." - }, - "forms": { - "type": "object", - "description": "A map of CLDR plural categories to their corresponding strings.", - "properties": { - "zero": { - "$ref": "common_types.json#/$defs/DynamicString", - "description": "String for the 'zero' category (e.g., 0 items)." - }, - "one": { - "$ref": "common_types.json#/$defs/DynamicString", - "description": "String for the 'one' category (e.g., 1 item)." - }, - "two": { - "$ref": "common_types.json#/$defs/DynamicString", - "description": "String for the 'two' category (used in Arabic, Welsh, etc.)." - }, - "few": { - "$ref": "common_types.json#/$defs/DynamicString", - "description": "String for the 'few' category (e.g., small groups in Slavic languages)." - }, - "many": { - "$ref": "common_types.json#/$defs/DynamicString", - "description": "String for the 'many' category (e.g., large groups in various languages)." - }, - "other": { - "$ref": "common_types.json#/$defs/DynamicString", - "description": "The default/fallback string (used for general plural cases)." - } - }, - "required": ["other"], - "additionalProperties": false - } - }, - "required": ["forms"] + "type": "object", + "properties": { + "value": { + "$ref": "common_types.json#/$defs/DynamicNumber", + "description": "The numeric value used to determine the plural category." + }, + "zero": { + "$ref": "common_types.json#/$defs/DynamicString", + "description": "String for the 'zero' category (e.g., 0 items)." + }, + "one": { + "$ref": "common_types.json#/$defs/DynamicString", + "description": "String for the 'one' category (e.g., 1 item)." + }, + "two": { + "$ref": "common_types.json#/$defs/DynamicString", + "description": "String for the 'two' category (used in Arabic, Welsh, etc.)." + }, + "few": { + "$ref": "common_types.json#/$defs/DynamicString", + "description": "String for the 'few' category (e.g., small groups in Slavic languages)." + }, + "many": { + "$ref": "common_types.json#/$defs/DynamicString", + "description": "String for the 'many' category (e.g., large groups in various languages)." + }, + "other": { + "$ref": "common_types.json#/$defs/DynamicString", + "description": "The default/fallback string (used for general plural cases)." } - ], + }, + "required": ["value", "other"], "unevaluatedProperties": false } }, @@ -949,7 +907,7 @@ "CatalogComponentCommon": { "type": "object", "properties": { - "weight": { + "weight": { "type": "number", "description": "The relative weight of this component within a Row or Column. This is similar to the CSS 'flex-grow' property. Note: this may ONLY be set when the component is a direct descendant of a Row or Column." } @@ -979,15 +937,6 @@ "discriminator": { "propertyName": "component" } - }, - "valueParam": { - "type": "object", - "properties": { - "value": { - "description": "The value to check." - } - }, - "required": ["value"] } } -} \ No newline at end of file +} diff --git a/specification/v0_9/test/cases/contact_form_example_test.json b/specification/v0_9/test/cases/contact_form_example_test.json index 129ba7087..35c10a30f 100644 --- a/specification/v0_9/test/cases/contact_form_example_test.json +++ b/specification/v0_9/test/cases/contact_form_example_test.json @@ -69,7 +69,8 @@ "checks": [ { "call": "email", - "message": "Please enter a valid email address." + "message": "Please enter a valid email address.", + "args": { "value": { "path": "/contact/email" } } } ] }, @@ -121,4 +122,4 @@ } } ] -} \ No newline at end of file +} From 64bef4d2aa5aa6ba538a4e01cea2b71324db57d0 Mon Sep 17 00:00:00 2001 From: Greg Spencer Date: Wed, 28 Jan 2026 17:08:01 -0800 Subject: [PATCH 6/7] Use transport agnostic language --- specification/v0_9/docs/a2ui_protocol.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/specification/v0_9/docs/a2ui_protocol.md b/specification/v0_9/docs/a2ui_protocol.md index 30aecad62..7f55ca2d6 100644 --- a/specification/v0_9/docs/a2ui_protocol.md +++ b/specification/v0_9/docs/a2ui_protocol.md @@ -569,8 +569,8 @@ _Replace the entire data model:_ When `sendDataModel` is set to `true` for a surface, the client automatically appends the **entire data model** of that surface to the metadata of every message (such as `action` or user query) sent to the server that created the surface. The data model is included using the transport's metadata facility (e.g., the `metadata` field in A2A or a header in HTTP). The payload follows the schema in [`a2ui_client_data_model.json`](../json/a2ui_client_data_model.json). - **Targeted Delivery**: The data model is sent exclusively to the server that created the surface. Data cannot leak to other agents or servers. -- **Trigger:** Data is sent only when an A2A message is triggered (e.g., by a user action like a button click). Passive data changes (like typing in a text field) do not trigger a network request on their own; they simply update the local state, which will be sent with the next action. -- **Payload:** The data model is included in the A2A message metadata, tagged by its `surfaceId`. +- **Trigger:** Data is sent only when a client-to-server message is triggered (e.g., by a user action like a button click). Passive data changes (like typing in a text field) do not trigger a network request on their own; they simply update the local state, which will be sent with the next action. +- **Payload:** The data model is included in the transport metadata, tagged by its `surfaceId`. - **Convergence:** The server treats the received data model as the current state of the client at the time of the action. ## Client-side logic & validation From 99e7658bf55c1a48e68adafeaf84d11e3a939516 Mon Sep 17 00:00:00 2001 From: Greg Spencer Date: Wed, 28 Jan 2026 17:11:12 -0800 Subject: [PATCH 7/7] Add some spaces --- specification/v0_9/docs/evolution_guide.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specification/v0_9/docs/evolution_guide.md b/specification/v0_9/docs/evolution_guide.md index fb94fc123..ab566f52b 100644 --- a/specification/v0_9/docs/evolution_guide.md +++ b/specification/v0_9/docs/evolution_guide.md @@ -228,7 +228,7 @@ Specifying an unknown surfaceId will cause an error. It is recommended that clie - **String Formatting**: Introduced the `formatString` function, which supports `${expression}` syntax for interpolation. - **Unified Expression Language**: Allows embedding JSON Pointer paths (absolute and relative) and client-side function calls directly within the format string. -- **Nesting**: Supports recursive nesting of expressions (e.g., `${formatDate(value:${/timestamp}, format:'yyyy-MM-dd')}`). +- **Nesting**: Supports recursive nesting of expressions (e.g., `${formatDate(value: ${/timestamp}, format: 'yyyy-MM-dd')}`). - **Restriction**: String interpolation `${...}` is **ONLY** supported within the `formatString` function. It is not supported in general for string properties, in order to strictly separate data binding definitions from static content. - **Reason**: Improves readability for complex strings. Instead of generating complex nested JSON objects (like chained concatenations) to combine strings and data, the model can write natural-looking template literals within the `formatString` function.