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

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

Aperta
#2,154 1 commento 0 reazioni 0 assegnatari Vedi su GitHub
enhancement proposal
Lingua principale
Python
Stelle
15.9k
Fork
1.4k
Merge medio
1g 17h
PR unite (30g)
163

Descrizione

## 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.

Guida per i contributori

Apri la guida per i contributori

Valutazione

Questa issue non è ancora stata valutata.

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.