google / google/adk-python

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

Ouverte
#4,959 4 commentaires 0 réactions 1 personne assignée Réclamée par @GWeale Voir sur GitHub
needs review services
Langage dominant
Python
Étoiles
21.5k
Forks
4k
Merge moyen
1 j 14 h
PR mergées (30 j)
37

Description

# 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

---

Guide de contribution

Ouvrir le guide de contribution

Évaluation

Cette issue n'a pas encore été évaluée.

Recevez les nouvelles issues par e-mail

Un résumé court des issues GitHub adaptées aux débutants.