🔴 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:
- Register a plugin whose
before_run_callback returns types.Content.
- Have the same plugin record or modify events in
on_event_callback.
- Execute the runner and collect its output events.
- 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?:
🔴 Required Information
Describe the Bug:
When a plugin's
before_run_callbackreturnstypes.Content, the runnercreates an early-exit
Eventand persists/yields it directly. Unlike eventsproduced during normal execution, this event does not pass through
on_event_callback.This is inconsistent with the
BasePlugin.on_event_callbackcontract, whichdescribes 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:
BaseAgentasync execution;LlmAgentasync execution;Steps to Reproduce:
before_run_callbackreturnstypes.Content.on_event_callback.on_event_callbackis never invoked.Expected Behavior:
An early-exit event produced from
before_run_callbackshould follow the sameevent middleware path as other runner-produced events:
before_run → Event → on_event_callback → persist / yieldThis 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 / yieldso
on_event_callbackis skipped.Environment Details:
mainatc7ffcfa8(package version 2.8.0)Model Information:
before_run_callbackstops execution beforeany model request
🟡 Optional Information
Regression:
Not known. The behavior is present on the current
mainbranch.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:
How often has this issue occurred?: