google / google/adk-python

run_live invokes after-agent callbacks after end_invocation

Cerrado
#7,080 1 comentario 0 reacciones 1 asignado Reclamado por @sanketpatil06 Ver en GitHub
live
Lenguaje dominante
Python
Estrellas
21.5k
Forks
4k
Merge medio
1 d 14 h
PR fusionados (30 d)
37

Descripción

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

Guía de contribución

Abrir la guía de contribución

Evaluación

Este issue todavía no se ha evaluado.

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.