google / google/adk-python

run_live invokes after-agent callbacks after end_invocation

Chiusa
#7,080 1 commento 0 reazioni 1 assegnatario Rivendicata da @sanketpatil06 Vedi su GitHub
live
Lingua principale
Python
Stelle
21.5k
Fork
4k
Merge medio
1g 14h
PR unite (30g)
37

Descrizione

## 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.

Guida per i contributori

Apri la guida per i contributori

Valutazione

Questa issue non è ancora stata valutata.

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.