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
- Start a session, send a message, get a response (turn 1).
- Call
Runner.rewind_async(rewind_before_invocation_id=<turn1_invocation>).
- Send the same (or a different) message again (turn 2).
- 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:
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:
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
Summary
adk_events_to_messages(session.events)iterates all ADK session events without accounting for rewind markers. AfterRunner.rewind_async()is called, the session still contains the old (now-invalid) events —rewind_asyncappends a special event withactions.rewind_before_invocation_idinstead of deleting the rewound events. Any consumer that callsadk_events_to_messagesdirectly onsession.eventswill therefore receive the stale messages that were supposed to be discarded.Reproduction
Runner.rewind_async(rewind_before_invocation_id=<turn1_invocation>).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_asyncdoes not delete events; it appends a rewind-marker event:The ADK
Runneritself correctly filters these markers before running the LLM (using an internal_filter_rewound_events-style pass). Butadk_events_to_messagesbypasses that filtering — it does not checkevent.actions.rewind_before_invocation_id.Impact
Any endpoint or component that reconstructs message history from
session.eventsviaadk_events_to_messages— notably thePOST /agents/stateendpoint registered byadd_adk_fastapi_endpoint— will return incorrect history after a rewind. Frontend clients that use/agents/stateto 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
Runneruses internally before passing events toadk_events_to_messages:Alternatively,
adk_events_to_messagesitself could apply this filtering internally so callers do not need to know about rewind semantics.Related