Skip to content

before_run early-exit events bypass on_event_callback #7007

Description

@jaywang172

🔴 Required Information

Describe the Bug:

When a plugin's before_run_callback returns types.Content, the runner
creates an early-exit Event and persists/yields it directly. Unlike events
produced during normal execution, this event does not pass through
on_event_callback.

This is inconsistent with the BasePlugin.on_event_callback contract, which
describes the hook as running when the runner produces an event and allows
plugins to modify the event before it is persisted to the session service and
yielded to the caller.

The behavior is reproducible in all three runner paths:

  • legacy BaseAgent async execution;
  • node / LlmAgent async execution;
  • live runner execution.

Steps to Reproduce:

  1. Register a plugin whose before_run_callback returns types.Content.
  2. Have the same plugin record or modify events in on_event_callback.
  3. Execute the runner and collect its output events.
  4. Observe that the early-exit event is yielded and persisted, but
    on_event_callback is never invoked.

Expected Behavior:

An early-exit event produced from before_run_callback should follow the same
event middleware path as other runner-produced events:

before_run → Event → on_event_callback → persist / yield

This ensures plugins used for logging, auditing, redaction, metadata
enrichment, or other event processing also observe early-exit responses.

Observed Behavior:

The early-exit path currently behaves as:

before_run → Event → persist / yield

so on_event_callback is skipped.

Environment Details:

  • ADK Library Version: current main at c7ffcfa8 (package version 2.8.0)
  • Desktop OS: macOS
  • Python Version: 3.11.9; also reproduced in tests on 3.10–3.14

Model Information:

  • Are you using LiteLLM: No
  • Which model is being used: N/A; before_run_callback stops execution before
    any model request

🟡 Optional Information

Regression:

Not known. The behavior is present on the current main branch.

Logs:

N/A; no exception is raised. The callback is silently skipped.

Screenshots / Video:

N/A

Additional Context:

The same metadata, callback, and event-merge processing used for normal runner
events can also process the synthesized early-exit event. Persistence
eligibility should continue to be decided from the original event so the
existing live-event persistence policy does not change.

Minimal Reproduction Code:

import asyncio

from google.adk.agents import Agent
from google.adk.apps import App
from google.adk.events import Event
from google.adk.plugins import BasePlugin
from google.adk.runners import Runner
from google.adk.sessions import InMemorySessionService
from google.genai import types


class EarlyExitPlugin(BasePlugin):
  def __init__(self):
    super().__init__(name="early_exit")
    self.event_seen = False

  async def before_run_callback(self, *, invocation_context):
    return types.Content(
        role="model", parts=[types.Part(text="blocked by before_run")]
    )

  async def on_event_callback(
      self, *, invocation_context, event: Event
  ):
    self.event_seen = True
    return event


async def main():
  plugin = EarlyExitPlugin()
  sessions = InMemorySessionService()
  await sessions.create_session(
      app_name="app", user_id="user", session_id="session"
  )
  runner = Runner(
      app=App(
          name="app",
          root_agent=Agent(name="agent", model="gemini-2.5-flash"),
          plugins=[plugin],
      ),
      session_service=sessions,
  )

  events = [
      event
      async for event in runner.run_async(
          user_id="user",
          session_id="session",
          new_message=types.Content(
              role="user", parts=[types.Part(text="hello")]
          ),
      )
  ]

  print(events[0].content.parts[0].text)  # blocked by before_run
  print(plugin.event_seen)  # False (expected: True)


asyncio.run(main())

How often has this issue occurred?:

  • Always (100%)

Activity

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

Metadata

Metadata

Assignees

Labels

core[Component] This issue is related to the core interface and implementation

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions