google / google/adk-python

feat: add before_yield_callback to support post-persistence event filtering

Đang mở
#5,161 2 bình luận 0 reaction 2 người được giao Được @ankursharmas nhận Xem trên GitHub
core needs review
Ngôn ngữ chính
Python
Star
21.5k
Fork
4k
Merge trung bình
1 ngày 14 giờ
Pull request đã merge (30 ngày)
37

Mô tả

## Feature Request

### Problem

After #3990 is fixed (PRs #4239 / #5021), `on_event_callback` will run **before** `append_event`, ensuring plugin modifications are persisted. This is the correct fix for the persistence-consistency problem.

However, it eliminates the ability to **independently** control what gets persisted versus what gets yielded to external consumers. There is currently no plugin callback that runs **after** session persistence but **before** the event is yielded.

### Use Case

When integrating ADK with external protocols (e.g., AG-UI via `ag_ui_adk`), it is common to need:

- **Full data in the session** — the LLM needs complete tool call/response history and all state deltas for correct reasoning across invocations.
- **Filtered data in the yield** — the external consumer (UI client) should not receive certain internal state fields, or tool call/response events for backend-only tools that have no UI representation.

Examples:
- Stripping internal state keys (e.g., `workflow_metadata`, `internal_cache`) from `state_delta` before they reach the UI, while keeping them in the session for LLM context.
- Suppressing `function_call`/`function_response` parts for tools that are purely backend operations (e.g., `start_audience_creation`, `set_audience_name`) where the UI only cares about state changes, not the tool invocation itself.

### Current Workarounds

| Approach | Limitation |
|---|---|
| `on_event_callback` (current, pre-#3990) | Works accidentally because callback runs after persistence — breaks when #3990 fix merges |
| `on_event_callback` (post-#3990) | Modifications are persisted too — cannot keep full data in session |
| `temp:` state prefix | Only works for state fields; doesn't persist across invocations; no equivalent for tool calls |
| Filtering in external middleware (e.g., `EventTranslator`) | Requires modifying upstream dependencies; not always under the consumer's control |

### Proposed Solution

Add a `before_yield_callback` (or `on_event_yield_callback`) to `BasePlugin` that runs **after** `append_event` but **before** the event is yielded from the runner:

```python
class BasePlugin(ABC):
# Existing — runs before persistence (after #3990 fix)
async def on_event_callback(
self, *, invocation_context: InvocationContext, event: Event
) -> Optional[Event]:
"""Modify events before persistence and yielding."""
pass

# Proposed — runs after persistence, before yield
async def before_yield_callback(
self, *, invocation_context: InvocationContext, event: Event
) -> Optional[Event]:
"""Modify or filter the event before it is yielded to external consumers.

The event has already been persisted to the session at this point.
Returning a modified Event replaces what is yielded (session unaffected).
Returning None yields the original persisted event unchanged.
"""
pass
```

The runner change in `_process_event` (from PR #5021) would become:

```python
async def _process_event(event, *, should_append_event):
# Step 1: on_event_callback (modify for persistence + yield)
modified_event = await plugin_manager.run_on_event_callback(
invocation_context=invocation_context, event=event
)
final_event = modified_event or event

# Step 2: Persist
if should_append_event:
await self.session_service.append_event(session=session, event=final_event)

# Step 3: before_yield_callback (modify for yield only)
yield_event = await plugin_manager.run_before_yield_callback(
invocation_context=invocation_context, event=final_event
)
return yield_event or final_event
```

### Benefits

- **Backward compatible** — `before_yield_callback` defaults to `pass` (returns `None`), so existing plugins are unaffected.
- **Clean separation of concerns** — persistence modifications vs. yield filtering are independent.
- **Enables middleware integration patterns** — ADK + AG-UI, ADK + A2A, or any external protocol consumer can filter events without affecting session integrity.
- **Consistent with existing plugin lifecycle** — follows the same pattern as `before_tool_callback`/`after_tool_callback` split.

### Related Issues

- #3990 — `on_event_callback` executes after `append_event`, preventing plugin modifications from being persisted
- PR #4239 — fix: persist events after plugin `on_event_callback` modifications
- PR #5021 — fix: persist `on_event_callback` mutations before session append

Hướng dẫn đóng góp

Mở hướng dẫn đóng góp

Đánh giá

Issue này chưa được đánh giá.

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.