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.
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.
- Press play. Each row is one write, by the agent or by the person.
Empty until the agent loads the plan (a STATE_SNAPSHOT).
The form appears when state arrives.
| Same timeline, three outcomes | Person edits lost | Agent writes held | Agent writes rejected | Conflicts for the person | Title right now |
|---|---|---|---|---|---|
| 0 | 0 | 0 | 0 | — | |
| 0 | 0 | 0 | 0 | — | |
| 0 | 0 | 0 | 0 | — |
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
}