Skip to content

Replace CRUD operations with JSON patch #1316

@jacobsimionato

Description

@jacobsimionato

Background

Currently, A2UI has an underlying data structure which is basically a set of components and a data model. But our messages are for incrementally updating each of them, just imply defining them.

Problems

  • There is no well-defined format to serialize the current state of a surface
  • Our CRUD operations are a little irregular. E.g. why updateComponents rather than updateSurface? Why not include initial components in CreateSurface etc (see createSurface that includes initial components and data model #1239). If we add more kinds of data, we need to invent new CRUD operations to represent them.
  • In practice, most UIs are created and never updated, so it makes sense for the format to emphasize the surface state, rather than incremental updates. Right now, we have an incremental API makes it simple to represent changes to state (a single message) but complex to represent the current state (list of message which may overwrite each other and thus be redundant).
  • There is no way to delete components currently, only replace them.

Recent changes in direction

Since we defined A2UI v0.8 and v0.9, some things have changed about our direction which could motivate this design change.

  • There is more emphasis on MCP, which is typically a "generate once" pattern, rather than a "generate and update" pattern. A2UI is kind of awkward for MCP, because they need to include a list of messages, and there is a question about which order they should be in etc (it doesn't matter).
  • We are looking at separating transport and inference format - https://docs.google.com/document/d/1mH2DJ_jZZA2vZ9EQlWaU4v8_xCAcmqrox_LlElmJ3ws/edit?tab=t.0#heading=h.c0uts5ftkk58 . LLMs cannot write JSON patch well, and this motivated us to avoid it previously. But, if we are defining a separate LLM format anyway, let's go ahead and use it seeing as it entirely solves the issue of representing incremental data updates.

Proposal

Let's focus on defining a JSON object which represents the state of a surface including data model and components. Then, when creating new surfaces, we just send the data. To update a surface, we send a JSON patch based on the previous data to apply.

Pros and Cons

Pros

  • Simplify the transport format
  • Simplify the mental model of a Surface snapshot and incremental updates
  • Make it easy for us to expand the surface state representation (e.g. add something more than data model and components) without needing to rethink CRUD operations
  • Simplify core library implementation. Currently, we have a message processor that understands how to apply our messages to a data model. Instead, we would just have some JSON and a JSON patch library that modifies it.
  • Obvious way to represent surface snapshot state to LLMs. Currently, in the most simple a2ui implementations, the LLM would see the history of A2UI messages in its context. This is unnecessarily verbose. If there are many messages updating a surface, it should likely just see a snapshot of surface state. This new structure makes it obvious how to achieve this.
  • Ability to represent arbitrarily small updates to state, e.g. update an individual property value

Cons

  • Creates more churn - this is relatively big change to the spec. Though, I believe we can make this update in the core library and renderers would be unaffected.
  • Agents will really need full access to the accurate existing UI state in order to generate correct patches. They will probably have this anyway, though.
  • JSON patch is hard for humans to read
  • To incrementally update surfaces, developers will need an inference library to help. They won't be able to wing the JSON patch generation.

Sketch

Surface

{
  "$defs": {
    "Surface": {
      "type": "object",
      "description": "The complete state of an A2UI surface.",
      "properties": {
        "surfaceId": {
          "type": "string",
          "description": "Unique identifier for the surface."
        },
        "catalogId": {
          "type": "string",
          "description": "The URI of the catalog defining the components used."
        },
        "components": {
          "type": "object",
          "description": "A map of ComponentId to component definitions.",
          "additionalProperties": {
            "$ref": "catalog.json#/$defs/anyComponent"
          }
        },
        "dataModel": {
          "type": "object",
          "description": "The initial data model for the surface.",
          "default": {}
        },
        "theme": {
          "$ref": "catalog.json#/$defs/theme"
        },
        "sendDataModel": {
          "type": "boolean",
          "default": false
        }
      },
      "required": ["surfaceId", "catalogId", "components"]
    }
  }
}

Server to client

{
  "oneOf": [
    { "$ref": "#/$defs/CreateSurfaceMessage" },
    { "$ref": "#/$defs/UpdateSurfaceMessage" },
    { "$ref": "#/$defs/DeleteSurfaceMessage" }
  ],
  "$defs": {
    "CreateSurfaceMessage": {
      "type": "object",
      "properties": {
        "version": { "const": "v0.10" },
        "createSurface": {
          "type": "object",
          "properties": {
            "surface": { "$ref": "common_types.json#/$defs/Surface" }
          },
          "required": ["surface"]
        }
      },
      "required": ["createSurface", "version"]
    },
    "UpdateSurfaceMessage": {
      "type": "object",
      "properties": {
        "version": { "const": "v0.10" },
        "updateSurface": {
          "type": "object",
          "properties": {
            "surfaceId": { "type": "string" },
            "patch": {
              "type": "array",
              "items": { "$ref": "#/$defs/JsonPatchOperation" }
            }
          },
          "required": ["surfaceId", "patch"]
        }
      },
      "required": ["updateSurface", "version"]
    },
    "JsonPatchOperation": {
      "type": "object",
      "properties": {
        "op": { "enum": ["add", "remove", "replace", "move", "copy", "test"] },
        "path": { "type": "string", "description": "JSON Pointer to the surface property." },
        "value": { "description": "The value to add or replace." },
        "from": { "type": "string", "description": "Required for move/copy." }
      },
      "required": ["op", "path"]
    }
  }
}

Metadata

Metadata

Assignees

Type

No type
No fields configured for issues without a type.

Projects

Status

Todo

Relationships

None yet

Development

No branches or pull requests

Issue actions