ag-ui-protocol / ag-ui-protocol/ag-ui
[Feature] Standardize conversation rewind / edit / regenerate (time-travel) at the protocol level
- Lenguaje dominante
- Python
- Estrellas
- 15.9k
- Forks
- 1.4k
- Merge medio
- 1 d 17 h
- PR fusionados (30 d)
- 163
Descripción
## Summary
There is no protocol-level concept for **rewinding, editing, or regenerating** a conversation ("time travel"): reverting the thread to an earlier point, optionally editing a past user message, and re-running from there. Today this is left entirely to each integration, which has to **infer** the intent by diffing the incoming `messages` against its own persisted history. One integration (LangGraph) implements it with heuristics; others (e.g. the ADK middleware) don't implement it at all. The result is inconsistent behavior across servers and a recurring class of subtle bugs.
I'd like to discuss standardizing this at the protocol level so clients can express "rewind/edit here" explicitly and every integration handles it the same way.
## Why this matters
### Rewind / edit is a first-class chat UX
Nearly every chat UI offers "edit your message and resend", "regenerate this response", or "go back to here". These all mean the same thing to the agent: **discard everything after some anchor point and continue from there** (optionally with an edited message). It's a core interaction, not an edge case.
### Today it's inferred, not declared
Because the protocol has no signal for it, a server can only guess from the message list. The reference implementation (LangGraph integration) does this in `prepare_stream`:
- **Edit detection**: an incoming message with the **same `id`** but **different content** than the stored one → treat as an edit → regenerate.
- **Truncation/time-travel detection**: the stored history has **more messages** than the incoming list, and the incoming ids are **not a subset** (i.e. not a plain continuation) → treat as a rewind → fork before the last incoming user message.
This is clever, but it's a heuristic reconstructing intent the client already knew. The **message `id` is the de-facto anchor**, yet nothing in the spec says so or defines the semantics.
### It's inconsistent across integrations
Since it lives in integration code, behavior diverges:
- The LangGraph integration supports edit/regenerate via checkpoint forking (time travel).
- Other middlewares don't detect divergence at all — they track "seen" message ids and only **append** unseen messages. An edited same-id message is silently dropped (treated as already processed), and a truncated list doesn't rewind — the new message is just stacked on top of the full history.
So the same client gesture ("edit and resend") produces different results depending on which server is behind AG-UI, which defeats the point of a shared protocol.
### It's a recurring source of bugs
The heuristic approach has generated a trail of issues in just one integration, e.g.:
- #1748 — same-id content edits missed by id-only comparison (client edit silently lost).
- #2109 — time travel not triggered when regenerating the very first assistant message.
- #1749 / #900 / #683 — fork/regenerate paths dropping config, or missing context parameters.
These are all symptoms of intent being **inferred** rather than **declared**.
## Current behavior (for reference)
The client keeps the message list and re-sends it on each run. To "rewind/edit", a UI either truncates or edits its local list and calls the normal run. The server must then diff that list against its persisted state and decide: continuation? edit? rewind? — with the message `id` as the only anchor, and no spec'd rules.
## Questions / possible directions (for discussion)
1. **An explicit optional field on `RunAgentInput`** — e.g. `rewindBeforeMessageId` (or `forkFromMessageId`). When present, the server reverts the thread to before that message and runs from there; the last message in the array is the (possibly edited) turn to run. Minimal, declarative, transport-agnostic, and removes all guessing. Servers that can't rewind can reject it explicitly instead of silently misbehaving.
2. **A documented divergence convention** — keep the "diff the messages array" approach but **specify it**: message `id` is the anchor; define precisely what counts as a continuation vs. an edit vs. a rewind, and mandate that integrations honor it. Cheaper for clients (no new field) but still relies on every server implementing the same diff.
3. **A lifecycle/emitted signal that a rewind happened** — so other observers/clients on the same thread stay consistent (analogous to a state reset), rather than only mutating server-side. Could pair with (1) or (2).
Trade-offs: (1) is the most explicit and least error-prone but touches the input schema; (2) avoids schema changes but keeps the heuristic burden on every integration; (3) is about keeping multiple clients in sync after a rewind.
Underlying engines already expose the needed primitive (e.g. LangGraph checkpoint forking via `aupdate_state`; ADK's `Runner.rewind_async(rewind_before_invocation_id=...)`), so a standard signal would mostly be about **plumbing an explicit anchor through** to that primitive, consistently.
## Related
- #1748, #1749, #900, #683, #2109 — bugs stemming from the inferred edit/regenerate/time-travel path in the LangGraph integration.
- #2031 — adk-middleware run lifecycle work (interrupt-aware); a declared rewind signal would fit naturally alongside it, since the ADK middleware currently has no rewind handling.
## Offer
Happy to help move this forward — e.g. prototype option (1) end to end in the ADK middleware (`ag-ui-adk`), where rewind isn't handled today, mapping an incoming anchor to `Runner.rewind_async(...)`. Flagging it as design-first since option (1) touches the core input schema.
Guía de contribución
Evaluación
Este issue todavía no se ha evaluado.