Skip to content

Commit 0e263f1

Browse files
gautamvarmadatlawuliang229
authored andcommitted
fix(agents): persist output_key when before_agent_callback short-circuits LlmAgent
Merge google#4838 - Closes: google#4837 **Problem:** When `LlmAgent` uses both `output_key` and `before_agent_callback`, and the before-agent callback returns `types.Content`, execution is correctly short-circuited and the response is returned to the user, but `session.state[output_key]` is not updated because the normal `LlmAgent` output-saving path is bypassed. **Solution:** Override `_handle_before_agent_callback` in `LlmAgent` to call `__maybe_save_output_to_state()` on any returned event. This keeps the fix scoped to `LlmAgent`, which owns `output_key`, avoids introducing `BaseAgent` changes for subclass-specific behavior, and fixes both `run_async` and `run_live` through the shared callback path. ### Testing Plan Added a regression test that runs `LlmAgent` with `output_key` and a short-circuiting `before_agent_callback`, then checks that `session.state["result"]` contains the returned text. **Unit Tests:** - [X] I have added or updated unit tests for my change. - [X] All unit tests pass locally. **Manual End-to-End (E2E) Tests:** Run repro code in google#4837 In that example, `session.state["result"]` should equal "cached answer" ### Checklist - [X] I have read the [CONTRIBUTING.md](https://github.com/google/adk-python/blob/main/CONTRIBUTING.md) document. - [X] I have performed a self-review of my own code. - [X] I have commented my code, particularly in hard-to-understand areas. - [X] I have added tests that prove my fix is effective or that my feature works. - [X] New and existing unit tests pass locally with my changes. - [X] I have manually tested my changes end-to-end. - [X] Any dependent changes have been merged and published in downstream modules. Co-authored-by: Liang Wu <wuliang@google.com> COPYBARA_INTEGRATE_REVIEW=google#4838 from gautamvarmadatla:fix/output-key-before-agent-callback 7f084e0 PiperOrigin-RevId: 934022327
1 parent 1c7255b commit 0e263f1

2 files changed

Lines changed: 33 additions & 0 deletions

File tree

src/google/adk/agents/llm_agent.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -491,6 +491,15 @@ class LlmAgent(BaseAgent, abc.ABC):
491491
"""
492492
# Callbacks - End
493493

494+
@override
495+
async def _handle_before_agent_callback(
496+
self, ctx: InvocationContext
497+
) -> Optional[Event]:
498+
event = await super()._handle_before_agent_callback(ctx)
499+
if event is not None:
500+
self.__maybe_save_output_to_state(event)
501+
return event
502+
494503
@override
495504
async def _run_async_impl(
496505
self, ctx: InvocationContext

tests/unittests/agents/test_llm_agent_output_save.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,16 @@
1717
import logging
1818
from unittest.mock import patch
1919

20+
from google.adk.agents.callback_context import CallbackContext
2021
from google.adk.agents.llm_agent import LlmAgent
2122
from google.adk.events.event import Event
2223
from google.adk.events.event_actions import EventActions
2324
from google.genai import types
2425
from pydantic import BaseModel
2526
import pytest
2627

28+
from .. import testing_utils
29+
2730

2831
class MockOutputSchema(BaseModel):
2932
message: str
@@ -277,6 +280,27 @@ def test_maybe_save_output_to_state_handles_empty_final_chunk_with_schema(
277280
# should remain empty.
278281
assert len(event.actions.state_delta) == 0
279282

283+
@pytest.mark.asyncio
284+
async def test_output_key_saved_when_before_agent_callback_short_circuits(
285+
self,
286+
):
287+
"""Test that output_key is written to session state when
288+
before_agent_callback short-circuits the agent."""
289+
290+
def cache_callback(callback_context: CallbackContext) -> types.Content:
291+
return types.Content(parts=[types.Part.from_text(text="cached answer")])
292+
293+
agent = LlmAgent(
294+
name="test_agent",
295+
output_key="result",
296+
before_agent_callback=cache_callback,
297+
)
298+
299+
runner = testing_utils.InMemoryRunner(agent)
300+
await runner.run_async("hello")
301+
302+
assert runner.session.state.get("result") == "cached answer"
303+
280304
def test_maybe_save_output_to_state_skips_function_response_only_event(self):
281305
"""Test that state_delta set by callback is not overwritten when event
282306

0 commit comments

Comments
 (0)