Skip to content

Add get_events() and filter_events() methods to Session #4959

@ecanlar

Description

@ecanlar

Feature Request: Add get_events() and filter_events() methods to Session

Problem Statement

Currently, when working with sessions that have been rewound, there is no built-in way to filter out events that have been invalidated by rewind operations. This forces every consumer to implement their own filtering logic, leading to code duplication, inconsistency, and potential bugs.

Current Situation

After the rewind feature was introduced in 9dce06f, session.events returns all events, including those that have been annulled by a rewind. When a rewind occurs, an event with actions.rewind_before_invocation_id is appended, but all previous events from that invocation onwards remain in the events list.

This means that when iterating over session events, we see "ghost" events that are no longer part of the active conversation flow.

Example of the Problem

# Session with events:
# - inv-1: user asks "What is Python?"
# - inv-1: agent responds "Python is a programming language"
# - inv-2: user asks "What is Java?"
# - inv-2: agent responds "Java is a programming language"
# - inv-rewind: rewind_before_invocation_id = "inv-2"
# - inv-3: user asks "What is JavaScript?"

# Current behavior - we see ALL events including the rewound inv-2:
for event in session.events:
    print(event.content)
# Output shows 6 events, including the "ghost" inv-2 events

# What we want - only see active events:
for event in session.filter_events():
    print(event.content)
# Output should show only 3 events: inv-1 and inv-3

Why This Is Needed

1. Code Duplication

Every consumer needs to implement the same filtering logic. In our codebase, we had to implement:

@staticmethod
def _filter_rewound_events(events: list) -> list:
    """Filter out events that have been annulled by a rewind."""
    filtered = []
    i = len(events) - 1
    while i >= 0:
        event = events[i]
        if event.actions and event.actions.rewind_before_invocation_id:
            rewind_invocation_id = event.actions.rewind_before_invocation_id
            for j in range(0, i):
                if events[j].invocation_id == rewind_invocation_id:
                    i = j
                    break
        else:
            filtered.append(event)
        i -= 1
    filtered.reverse()
    return filtered

This is 15+ lines of non-trivial logic that every ADK consumer must write.

2. Error-Prone

The rewind filtering algorithm is complex and easy to get wrong:

  • Must iterate backward through events
  • Must handle multiple sequential rewinds
  • Must maintain chronological order after filtering
  • Edge cases (rewind to first invocation, missing target, etc.)

3. Inconsistent Behavior

Different consumers may implement filtering differently, leading to:

  • Different interpretations of what "rewound" means
  • Subtle bugs in edge cases
  • Harder to debug issues across different codebases

4. Breaking the Abstraction

Consumers shouldn't need to understand the internal structure of rewind events (actions.rewind_before_invocation_id) to work with sessions. This is an implementation detail that should be hidden behind a clean API.

Real-World Use Case

In our agent system, we need to:

  1. Display conversation history to users - we only want to show active messages, not rewound ones
  2. Process events for analytics - we need to count actual user interactions, excluding rewound attempts
  3. Build context for the LLM - when constructing prompts, we only want to include active conversation history

Currently, we must manually filter events in all these places:

# In our chat controller
for event in self._filter_rewound_events(session.events):
    if event.author == "user":
        conversation_history.append(self._extract_text(event))

# In our analytics service
active_events = self._filter_rewound_events(session.events)
user_messages = [e for e in active_events if e.author == "user"]

# In our context builder
for event in self._filter_rewound_events(session.events):
    context += self._format_event(event)

Proposed Solution

Add two methods to the Session class:

get_events() -> list[Event]

Returns all events in the session (same as session.events but as a method for API consistency).

filter_events(*, exclude_rewound: bool = True) -> list[Event]

Returns filtered events, with the primary use case being exclusion of rewound events.

# Get only active events (default behavior)
for event in session.filter_events():
    process(event)

# Get all events including rewound ones
for event in session.filter_events(exclude_rewound=False):
    process_all(event)

Benefits

  1. Cleaner code - One-line filtering instead of 15+ lines
  2. Consistent behavior - All consumers use the same filtering logic
  3. Fewer bugs - Centralized, well-tested implementation
  4. Better abstraction - Hides implementation details of rewind
  5. Backward compatible - session.events still works as before

Additional Context

  • The rewind feature is relatively new and not all consumers may be aware of the need to filter
  • GetSessionConfig provides num_recent_events and after_timestamp filtering, but no way to filter rewound events
  • This pattern (filtering by state) is common in other session-based frameworks

Related Pull Request

I've already implemented this feature and submitted a PR: #4960


Metadata

Metadata

Assignees

Labels

needs review[Status] The PR/issue is awaiting review from the maintainerservices[Component] This issue is related to runtime services, e.g. sessions, memory, artifacts, etc

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions