Skip to content

Conversation

@gspencergoog
Copy link
Collaborator

@gspencergoog gspencergoog commented Jan 10, 2026

Summary

Introduces a CRDT-based bidirectional data synchronization mechanism to A2UI v0.9 using Hybrid Logical Clocks (HLC) and Version Vectors. This replaces the simple path/value replacement model with a more resilient system capable of handling multi-actor conflicts and offline-first scenarios.

Changes

  • Protocol Documentation: Added a comprehensive "Data Model Updates: Synchronization and Convergence" section to a2ui_protocol.md. This covers HLCs, fractional indexing for ordered lists, JSON Pointer constraints, and tombstone handling for deletions.
  • Schema Enhancements:
    • common_types.json: Defined HlcString, DataUpdate, and VersionVector structures.
    • server_to_client.json: Refactored UpdateDataModelMessage to support batch updates with actor IDs and version vectors. Added WatchDataModelMessage for configuring client update modes (onAction vs onChanged).
    • client_to_server.json: Added the dataModelChanged message type to allow Renderers to push state updates back to Agents.
  • Testing: Added client_messages.json test cases to validate the new message structures in client_to_server.json.

Example Usage

An example of the new updateDataModel message format, incorporating HLCs and Version Vectors for synchronization:

{
  "updateDataModel": {
    "surfaceId": "contact_form_1",
    "actorId": "agent-123",
    "updates": [
      {
        "path": "/user/email",
        "value": "[email protected]",
        "hlc": "2026-01-12T16:55:00.000Z:0001:agent-123"
      }
    ],
    "versions": {
      "agent-123": "2026-01-12T16:55:00.000Z:0001:agent-123",
      "renderer-456": "2026-01-12T16:54:30.000Z:0005:renderer-456"
    }
  }
}

Impact & Risks

  • BREAKING CHANGE: The UpdateDataModelMessage (updateDataModel) structure is no longer compatible with previous v0.9 drafts. It now requires an updates array and a versions vector.
  • Implementation Overhead: Both clients and servers must now implement HLC generation and lexicographical comparison logic to ensure convergence.
  • No Native Arrays: The protocol now explicitly forbids JSON arrays in the data model updates to avoid index-based conflicts, requiring implementations to use fractional indexing with stable IDs.

Testing

  • Verified schema changes against the new test cases in specification/0.9/test/cases/client_messages.json.
  • Manual review of the documentation examples in a2ui_protocol.md to ensure they match the updated schema.
  • To verify the changes locally:
    1. Run python specification/0.9/test/run_tests.py (if applicable) or validate the JSON schemas using a standard validator.
    2. Check that the updateDataModel example in a2ui_protocol.md complies with the updated server_to_client.json schema.

- `updateComponents`: Provides a list of component definitions to be added to or updated in a specific surface.
- `updateDataModel`: Provides new data to be inserted into or to replace a surface's data model.
- `deleteSurface`: Explicitly removes a surface and its contents from the UI.
- `configureDataUpdates`: Configures how and when the client sends data model updates to the server.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we put this in createSurface perhaps?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like it's a different kind of configuration. You also might want it to change after creation (listen to something else, stop listening), and createSurface isn't something that feels like it can be called more than once.


- `surfaceId` (string, required): The unique identifier for the UI surface to be configured.
- `configurations` (array, required): A list of configuration rules.
- `path` (string, required): The data path to configure.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is more configuration than needs to be in the protocol. Ontimeout and immediate are kind of similar anyway from the servers perspective because even immediate updates are affected by network latency. I think we could do just inaction and immediate, with rate limiting etc as an implementation setups.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

the usecases are either 1) the user says "submit form" in the chat window (onAction covers this) and 2) some realtime validation of fields ("immediate" covers this)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahh, OK. I went the other way and eliminated immediate and kept onTimeout, since you can set the timeout to zero, but if you don't think we need the timeout config, then I can remove onTimeout and go back to immediate, (or call it onChanged, which might be a better name).

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed to onChanged and made the timeout default to zero.

- `mode` (string, required): One of `onAction`, `onTimeout`, or `immediate`.
- `timeoutMs` (integer, optional): Required if `mode` is `onTimeout`.

**Nested Path Precedence:**
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems overly complex. How about instead we always send the whole data model, perhaps as a JSON patch relative to the last snapshot that was transmitted? That way, we have minimal configuration, minimal payload size, and maximum sync. The agent can choose to ignore some of the data if it wants.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I debated about including paths too.

Deltas can be problematic and complex too: if you drop one, or misapply one, the whole thing breaks because they're context dependent. Applying the deltas can all happen outside of the LLM, so probably, with testing and error recovery, we could make it robust. I do worry about sync issues: if a change is on its way from the server, and the client sends a delta that doesn't yet include the change, but the server thinks it does, then things get out of sync. This is a solvable problem (delta sequence IDs, state hashes, and a way to grab the "full state" from the source of truth if there's a sync issue), but with delta updates going both directions (the updateDataModel is a delta update too), things can get out of sync easily, and you have to have a way to rebase local changes onto the source of truth when they get out of sync. I especially worry about this if the JSON patch is patching a list, since it relies on list indices.

One other reason I did end up including paths was that way we could avoid putting the entire data model into the context each time: you could just include the data from the path that changed, since that was what the agent was interested in. It keeps the context focused on the relevant information, and makes the cadence of the changes match the thing that the agent is interested in. I was worried that if you send updates every time anything changes, you might get a bunch of irrelevant updates that force extra inferences because there's no specificity so the agent has to use the LLM to evaluate each change. You can do your own filtering and change detection inside the agent, though, and filter what you send to the LLM, so this can be mitigated.

Some of this might be easier if we could assume that the client or server was the source of truth for the data model, but it is useful that the agent can update data model information and the client will reflect it, and there seem to be use cases where people want updates to go client->server. Up until adding this feature (client->server updates), we could assume that the server was the source of truth.

Could this entire feature be implemented using action events? The onAction mode is basically the same as the action on a button, but allows more global configuring of the context that is sent and a more global triggering criteria.

Copy link
Collaborator

@wrenj wrenj Jan 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Least confusing behavior is whichever rule sends the update first gets used. So realtime always trumps onAction. Should we use that?

eg
/user is set to realtime
/user/name is set to onAction
behavior: /user is updated in realtime, including /user/name since onAction always sees no updates to send when triggered

or
/user/name is set to realtime
/user/name is set to onAction
behavior: /user/name is updated in realtime, since onAction always sees no updates to send when triggered

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also we should say client is the source of truth, right?

When the user says "ok i filled out my time off form go ahead and submit it" we need to be 100% accurate with what the agent submits

- `surfaceId` (string, required): The unique identifier for the UI surface to be configured.
- `configurations` (array, required): A list of configuration rules.
- `path` (string, required): The data path to configure.
- `mode` (string, required): One of `onAction`, `onTimeout`, or `immediate`.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

onAction I think is confusing - mouseOver is an action. Is sending a message an action? its called message in a2a.

How about mode: onSend

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We already have "action" defined on buttons, which is used to describe a user action that triggers a message to the server with context. So, I was naming this to indicate that when one of those actions is triggered, this mode piggybacks another event (the dataModelChanged event) onto the action's event.

I'm kind of wondering if we couldn't get rid of the "onAction" mode altogether and just have a way to add extra context to all actions.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I meant when a user types in a query and hits send that happens outside of the A2UI framework. We want that to send any buffered A2UI client data model changes too. onAction doesn't imply that behavior

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahh, I see. I have been focusing only on the A2UI framework's responsibilities. Having an external trigger for forcing an update event does seem useful.

onSend doesn't seem quite right though: makes me think "So..I send on a send?" How about "onRequest"? Also, maybe we change the config words from event-style names to be something like "request", and "timeout"?

@gspencergoog
Copy link
Collaborator Author

gspencergoog commented Jan 13, 2026

Okay, I updated the data model description to be a robust mechanism for a distributed data sync where the renderer is the source of truth for the data model, but the agent can update it, and they both have a synchronized version of the data model that will converge to the same data and be consistent.

The idea is that the agent will handle the actual sync, and the LLM inferences will just deal with JSON input and generate JSON patch mutations which the agent turns into data model updates on the wire. I updated the PR description to match.

@gspencergoog
Copy link
Collaborator Author

Okay, since this isn't an approved spec yet, I'm going to commit this, and we can continue to iterate.

@gspencergoog gspencergoog merged commit 2925b5b into google:main Jan 13, 2026
5 checks passed
}
}
},
"HlcString": {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

avoid acronyms

HybridLogicalClockString

{
"path": "/user/name",
"value": "Jane Doe",
"hlc": "2026-01-12T16:34:29.000Z:0001:agent-1"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like this might be overkill, we only have two actors (client and server), and a clear requirement of client always wins (when I hit submit what the user saw when they hit it is what matters). I think the only thing that needs versioning is to make sure a server update doesn't get applied after the client changed it. Would a simple incrementing version_id work for this then? And if there is a conflict (server and client use the same version_id) then the client wins. wdyt?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're describing the Operational Transform sync method, and it requires an ack for each change from the renderer, which seemed like a lot of back and forth to me, especially given that LLM inferences are slow. The renderer state might change many times while the LLM inference was running. OT would also require the agent to rebase its model each time a new change comes from the renderer, which might mean re-running inferences to get the correct data change. CRDT just requires it to apply changes and keep the HLC value.

Also, we might easily end up with more than two actors: e.g. local stored state, or an additional agent, and CRDT handles that and asynchrony (like going offline, or long inference times) inherently.

I realize that it's complex, but it also addresses a lot of issues.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants