ag-ui-protocol / ag-ui-protocol/ag-ui
[Bug] adk_events_to_messages returns stale messages after rewind — rewound events are not filtered
- Vorherrschende Sprache
- Python
- Sterne
- 15.9k
- Forks
- 1.4k
- Ø Merge
- 1 T. 17 Std.
- Gemergte PRs (30 T.)
- 163
Beschreibung
## Summary
`adk_events_to_messages(session.events)` iterates all ADK session events without accounting for rewind markers. After `Runner.rewind_async()` is called, the session still contains the old (now-invalid) events — `rewind_async` appends a special event with `actions.rewind_before_invocation_id` instead of deleting the rewound events. Any consumer that calls `adk_events_to_messages` directly on `session.events` will therefore receive the stale messages that were supposed to be discarded.
## Reproduction
1. Start a session, send a message, get a response (turn 1).
2. Call `Runner.rewind_async(rewind_before_invocation_id=)`.
3. Send the same (or a different) message again (turn 2).
4. Call `adk_events_to_messages(session.events)`.
**Expected:** messages from turn 2 only (or turn 1 + turn 2 if only part of turn 1 was rewound).
**Actual:** messages from both the rewound turn 1 AND turn 2 — the stale messages reappear.
## Root cause
`Runner.rewind_async` does not delete events; it appends a rewind-marker event:
```python
rewind_event = Event(
invocation_id=new_invocation_context_id(),
author='user',
actions=EventActions(
rewind_before_invocation_id=rewind_before_invocation_id,
state_delta=state_delta,
artifact_delta=artifact_delta,
),
)
await self.session_service.append_event(session=session, event=rewind_event)
```
The ADK `Runner` itself correctly filters these markers before running the LLM (using an internal `_filter_rewound_events`-style pass). But `adk_events_to_messages` bypasses that filtering — it does not check `event.actions.rewind_before_invocation_id`.
## Impact
Any endpoint or component that reconstructs message history from `session.events` via `adk_events_to_messages` — notably the `POST /agents/state` endpoint registered by `add_adk_fastapi_endpoint` — will return incorrect history after a rewind. Frontend clients that use `/agents/state` to hydrate or display conversation history will show messages the user explicitly chose to discard.
## Suggested fix
Apply the same rewind-filtering pass that the ADK `Runner` uses internally before passing events to `adk_events_to_messages`:
```python
def filter_rewound_events(events: list) -> list:
filtered = []
i = len(events) - 1
while i >= 0:
event = events[i]
if event.actions and event.actions.rewind_before_invocation_id:
rewind_id = event.actions.rewind_before_invocation_id
for j in range(i):
if events[j].invocation_id == rewind_id:
i = j
break
else:
filtered.append(event)
i -= 1
filtered.reverse()
return filtered
# Then:
messages = adk_events_to_messages(filter_rewound_events(session.events))
```
Alternatively, `adk_events_to_messages` itself could apply this filtering internally so callers do not need to know about rewind semantics.
## Related
- #2154 — rewind / edit / regenerate at the protocol level (this bug is the history-consistency consequence of implementing rewind).
Beitragsleitfaden
Bewertung
Dieses Issue wurde noch nicht bewertet.