ag-ui-protocol / ag-ui-protocol/ag-ui

[Feature] Standardize conversation rewind / edit / regenerate (time-travel) at the protocol level

Abierto
#2,154 1 comentario 0 reacciones 0 asignados Ver en GitHub
enhancement proposal
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

Abrir la guía de contribución

Evaluación

Este issue todavía no se ha evaluado.

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.