Skip to content

feat: add before_yield_callback to support post-persistence event filtering #5161

Closed

Description

@anmol0e

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:

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:

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

Activity

  1. added
    core[Component] This issue is related to the core interface and implementation
    on Apr 5, 2026
  2. added theissue type on Apr 5, 2026
  3. self-assigned this
    on Apr 6, 2026
  4. klateefa commented on Apr 6, 2026

    @klateefa

    @anmol0e Thanks for the update. The request is currently being reviewed, and I will let you know if anything further is needed. Thanks for understanding.

  5. added
    needs review[Status] The PR/issue is awaiting review from the maintainer
    on Apr 6, 2026
  6. meIlikethis commented on Apr 16, 2026

    @meIlikethis

    Hi, I would like to work on this.
    Since there are no active PRs, could you please assign it to me?

  7. DeanChensj commented on Oct 14, 2026

    @DeanChensj
    Collaborator

    Hi @anmol0e, thank you for reporting this issue!

    This has been resolved by PR #7185 and merged into the main branch in commit 044a1ec.

    We are closing this issue as resolved. If you find that the issue persists on the latest ADK release or if this was closed in error, please feel free to reopen it or leave a comment! Thank you again for your contribution.

  8. github-actions commented on Oct 14, 2026

    @github-actions

    Are you satisfied with the resolution of your issue?
    Yes
    No

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

core[Component] This issue is related to the core interface and implementationneeds review[Status] The PR/issue is awaiting review from the maintainer

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions