google / google/adk-python

Add `get_events()` and `filter_events()` methods to Session

Abierto
#4,959 4 comentarios 0 reacciones 1 asignado Reclamado por @GWeale Ver en GitHub
needs review services
Lenguaje dominante
Python
Estrellas
21.5k
Forks
4k
Merge medio
1 d 14 h
PR fusionados (30 d)
37

Descripción

# 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](https://github.com/google/adk-python/commit/9dce06f9b00259ec42241df4f6638955e783a9d1), `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

```python
# 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:

```python
@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:

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

```python
# 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: https://github.com/google/adk-python/pull/4960

---

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.