Labs/Shared state and conflicts
04

Two writers, one state

An agent and a person edit the same trip plan at the same time. Switch the conflict strategy and watch who wins.

GoalLearn why shared agent state needs an ownership model, not just a sync mechanism.

bidirectional stateJSON Patchlast-writer-winsfield ownershiprebase
step 00 / 12

Press play. A scripted person edits the title and the sail note while the agent's patches for those same fields are in flight. Then switch strategy: the history re-folds instantly. You can always type into pane 3 yourself.

1 · Patch logagent + person
  1. Press play. Each row is one write, by the agent or by the person.
2 · Shared stateempty

Empty until the agent loads the plan (a STATE_SNAPSHOT).

3 · What the person seeseditable

The form appears when state arrives.

Same timeline, three outcomesPerson edits lostAgent writes heldAgent writes rejectedConflicts for the personTitle right now
0000—
0000—
0000—
How it works · and what real frameworks doclick to collapse

Sync is not the hard part. Moving JSON Patch ops between agent and browser is a solved problem: AG-UI carries them as STATE_DELTA events, and CopilotKit exposes the result as shared state in React. Deciding who wins when both sides write the same path is the part you have to design.

Ownership is a product decision. Holding agent writes as suggestions keeps the person in control, at the cost of the agent sometimes working from a plan the person never accepted.

Tests make writes honest. RFC 6902 already has a test op. Put the value the writer last saw in front of every write and stale writes fail loudly instead of silently clobbering. Graph runtimes such as LangGraph solve the agent side differently, by versioning state per checkpoint (Lab 06).

Many writers → CRDTs. When several people and agents edit one document, teams reach for CRDT libraries such as Yjs, where concurrent edits merge by construction and the agent becomes one more collaborator.

// rebase: the agent's write carries what it saw
const patch = [
  { op: "test",    path: "/activities/1/note", value: "" },
  { op: "replace", path: "/activities/1/note",
    value: "Departs 18:30 from Doca" },
]

try {
  state = applyPatch(state, patch)       // atomic
} catch {
  // stale: the person wrote "Bring jackets" first
  const cur = getAt(state, "/activities/1/note")
  state = applyPatch(state, [
    { op: "test",    path: "/activities/1/note", value: cur },
    { op: "replace", path: "/activities/1/note",
      value: cur + " · Departs 18:30 from Doca" },
  ])                                     // re-planned from fresh state
}