-
Notifications
You must be signed in to change notification settings - Fork 770
Add Client-to-Server Data Update Mechanism and Configuration #467
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
| - `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. |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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:** |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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`. |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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"?
c80baf6 to
1eed407
Compare
|
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. |
|
Okay, since this isn't an approved spec yet, I'm going to commit this, and we can continue to iterate. |
| } | ||
| } | ||
| }, | ||
| "HlcString": { |
There was a problem hiding this comment.
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" |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
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
a2ui_protocol.md. This covers HLCs, fractional indexing for ordered lists, JSON Pointer constraints, and tombstone handling for deletions.common_types.json: DefinedHlcString,DataUpdate, andVersionVectorstructures.server_to_client.json: RefactoredUpdateDataModelMessageto support batch updates with actor IDs and version vectors. AddedWatchDataModelMessagefor configuring client update modes (onActionvsonChanged).client_to_server.json: Added thedataModelChangedmessage type to allow Renderers to push state updates back to Agents.client_messages.jsontest cases to validate the new message structures inclient_to_server.json.Example Usage
An example of the new
updateDataModelmessage 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
UpdateDataModelMessage(updateDataModel) structure is no longer compatible with previous v0.9 drafts. It now requires anupdatesarray and aversionsvector.Testing
specification/0.9/test/cases/client_messages.json.a2ui_protocol.mdto ensure they match the updated schema.python specification/0.9/test/run_tests.py(if applicable) or validate the JSON schemas using a standard validator.updateDataModelexample ina2ui_protocol.mdcomplies with the updatedserver_to_client.jsonschema.