Synchronize frontend and backend state without losing user edits
How to study this lesson
Read one section, trace the worked example, and try the knowledge check. Bookmark saves a shortcut; notes appear in My notebook. Mark lesson complete is your own assessment and does not mark its lab passed.
By the end, you can
- Distinguish full snapshots from ordered state deltas.
- Assign ownership and revision rules to shared fields.
- Validate stale actions and recover from missing updates.
A snapshot establishes a baseline
Shared state lets an agent and a frontend coordinate on the same task representation. A snapshot provides a complete baseline, while a delta changes selected parts. AG-UI documents STATE_SNAPSHOT and STATE_DELTA; state deltas use JSON Patch operations, which must be applied in order. [AG-UI state management documentation](https://docs.ag-ui.com/concepts/state). A small patch can be efficient, but it is meaningful only relative to the expected state.
Suppose the frontend holds a report with title Draft and status reviewing. A delta changes status to ready. If the client missed an earlier patch that switched the report identifier, the same status change could appear on the wrong report. Keep entity identity and an application-level revision contract with state synchronization. Such revisions are your application's reliability design unless the chosen integration explicitly supplies them; do not present invented revision fields as mandatory AG-UI properties.
Assign ownership before choosing a merge strategy
Some fields are server-owned, such as verified task status or an artifact checksum. Others are local interaction state, such as whether a panel is expanded. A user-editable draft may need explicit coordination because both the user and agent can propose changes. Publishing the entire backend state to the frontend can expose secrets and make ownership unclear. Define a projection containing only the fields the interface needs.
Consider the user typing a report title while an agent finishes a summary. A broad snapshot that replaces the user's title with an older value loses work. Options include separate draft and committed fields, optimistic revision checks, or a documented conflict resolution policy. Last-write-wins is simple but may overwrite a deliberate user edit with a delayed agent update. Choose based on the meaning of each field, not on which merge algorithm is easiest to implement globally.
Apply patches atomically and validate their scope
A JSON Patch document can contain several operations whose meaning depends on order. A test operation can assert an expected value before replacements occur. If an operation fails, avoid exposing a half-applied local state. Apply to a copy, validate the result, and commit the new state only if the whole allowed update succeeds. Restrict which paths a remote agent may modify and reject changes to server-owned authorization fields.
The lesson code supports only top-level object fields and a small operation subset. It demonstrates atomicity and a failed precondition without claiming full RFC 6902 support. A production implementation must use a conforming parser, JSON Pointer escaping, array semantics, and the full selected operation rules, then add application validation. A syntactically legal patch that sets approved to true is still unauthorized if that field belongs to the verified user-action handler.
Bind actions to the state the user reviewed
An approval button should reference a concrete proposal and revision. Suppose the user reviews a message to customer A, but before the click reaches the server the agent revises the destination to customer B. The server should not execute the new proposal under the old click. Bind the action to the reviewed payload fingerprint, verify current authorization, and reject or re-present a changed proposal.
This is the same principle used for optimistic state updates, applied to user intent. The frontend may optimistically display submitting, but committed success must come from the authoritative handler after validation. Duplicate clicks should resolve to the same logical action result when supported, not cause repeated sends. UI component identity, action identity, task identity, and operation identity serve different purposes; keeping them separate makes a confusing retry trace much easier to diagnose.
Recover from divergence with a clear baseline
If a client detects a missing event or a patch precondition failure, stop applying dependent updates and request authoritative state through the supported integration. Preserve unsaved local drafts separately so resynchronization does not silently erase user work. Show a concise conflict when both sides changed the same meaningful field, and offer a deliberate resolution instead of pretending the states agree.
Build a test trace that starts with a snapshot, applies two patches, drops one patch, and attempts a user action from the stale view. The correct system detects the mismatch, avoids the external effect, and restores a valid baseline. Evaluate state consistency and user-edit preservation alongside rendering latency. A responsive UI that approves the wrong revision is incorrect. The lab and project implement a small local reducer so these invariants can be tested before adding real streaming adapters and a graphical renderer.
Work through the code
This teaching simulation implements only test and replace for plain top-level dictionary paths. It is not a complete JSON Patch implementation or AG-UI client. It copies state before applying operations so the input remains unchanged if validation fails.
from copy import deepcopy
def patch(state, operations):
candidate = deepcopy(state)
for operation in operations:
path = operation['path']
if not path.startswith('/') or '/' in path[1:] or '~' in path:
raise ValueError('only plain top-level paths supported')
key = path[1:]
if operation['op'] == 'test':
if key not in candidate or candidate[key] != operation['value']:
raise ValueError('precondition failed')
elif operation['op'] == 'replace':
if key not in candidate:
raise ValueError('missing key')
candidate[key] = deepcopy(operation['value'])
else:
raise ValueError('unsupported operation')
return candidate
original = {'revision': 2, 'status': 'reviewing'}
updated = patch(original, [{'op': 'test', 'path': '/revision', 'value': 2},
{'op': 'replace', 'path': '/status', 'value': 'ready'}])
print('before:', original['status'])
print('after:', updated['status'])
before: reviewing after: ready
Run Python snippets locally with the prerequisites named above. The module coding lab runs directly in your browser.
Pause and reason
A user approves proposal revision 4, but the server now holds revision 5 with a different recipient. What response should the action handler produce?
Check your understanding
Which state should be excluded from a general frontend shared-state snapshot?
Your notes
Explain the mechanism in your own words. Add a failure you want to test.