run_live invokes after-agent callbacks after end_invocation
- Vorherrschende Sprache
- Python
- Sterne
- 21.5k
- Forks
- 4k
- Ø Merge
- 1 T. 14 Std.
- Gemergte PRs (30 T.)
- 37
Beschreibung
## Describe the Bug
When an agent sets `InvocationContext.end_invocation` during its implementation,
`BaseAgent.run_async()` skips after-agent callbacks, but `BaseAgent.run_live()`
still invokes them. This includes both plugin and agent-defined
`after_agent_callback` hooks. A callback can therefore append an extra response
or mutate state after live execution has explicitly ended the invocation.
## Steps to Reproduce
1. Define a `BaseAgent` that yields an event and sets `ctx.end_invocation = True`.
2. Register an `after_agent_callback` that records its invocation.
3. Run the agent through `run_async()` and `run_live()` with fresh contexts.
4. Observe that only live execution invokes the after callback.
## Expected Behavior
Once the agent implementation ends the invocation, both entry points skip
after-agent hooks. Normal completion without `end_invocation` still invokes
them. Runner-level `after_run_callback` remains a separate lifecycle hook.
## Observed Behavior
On `main` at `a9cf81fe`, a four-case regression test (async/live × normal/ended)
reports three passes and one failure: the live ended case invokes the plugin
after-agent callback once instead of zero times.
## Environment Details
- ADK: source checkout at `a9cf81fe` (package version 2.8.0).
- OS: macOS.
- Python: 3.11.9.
- LiteLLM: No.
- Model: N/A; the reproduction uses a local agent and makes no model requests.
## Minimal Reproduction Code
```python
import asyncio
from google.adk.agents.base_agent import BaseAgent
from google.adk.agents.invocation_context import InvocationContext
from google.adk.events.event import Event
from google.adk.plugins.plugin_manager import PluginManager
from google.adk.sessions.in_memory_session_service import InMemorySessionService
from google.genai import types
class EndingAgent(BaseAgent):
async def _run_async_impl(self, ctx):
yield Event(
author=self.name,
content=types.Content(parts=[types.Part(text="agent output")]),
)
ctx.end_invocation = True
_run_live_impl = _run_async_impl
async def main():
for entrypoint in ("run_async", "run_live"):
calls = []
def after_agent(callback_context):
calls.append("after_agent")
agent = EndingAgent(name="agent", after_agent_callback=after_agent)
sessions = InMemorySessionService()
session = await sessions.create_session(app_name="app", user_id="user")
ctx = InvocationContext(
invocation_id="invocation",
agent=agent,
session=session,
session_service=sessions,
plugin_manager=PluginManager(plugins=[]),
)
async for _ in getattr(agent, entrypoint)(ctx):
pass
print(entrypoint, calls)
asyncio.run(main())
```
Current output:
```text
run_async []
run_live ['after_agent']
```
Expected: both lists are empty.
## Additional Context
The async entry point already checks `ctx.end_invocation` after consuming its
implementation generator. The live entry point checks it before execution but
not after. A focused fix can add the corresponding post-execution check in
`run_live()`, with regression coverage for both hooks and both execution modes.
Regression history: not established; this report does not claim a particular
release introduced the behavior. Reproduction frequency: always.
Beitragsleitfaden
Bewertung
Dieses Issue wurde noch nicht bewertet.