|
23 | 23 | from google.adk.agents.llm_agent import Agent |
24 | 24 | from google.adk.agents.loop_agent import LoopAgent |
25 | 25 | from google.adk.agents.run_config import RunConfig |
| 26 | +from google.adk.apps.app import ResumabilityConfig |
26 | 27 | from google.adk.events.event import Event |
27 | 28 | from google.adk.features import FeatureName |
28 | 29 | from google.adk.features._feature_registry import temporary_feature_override |
|
36 | 37 | from google.adk.models.llm_request import LlmRequest |
37 | 38 | from google.adk.models.llm_response import LlmResponse |
38 | 39 | from google.adk.plugins.base_plugin import BasePlugin |
| 40 | +from google.adk.sessions.in_memory_session_service import InMemorySessionService |
39 | 41 | from google.adk.tools.base_toolset import BaseToolset |
40 | 42 | from google.adk.tools.google_search_tool import GoogleSearchTool |
41 | 43 | from google.adk.utils.variant_utils import GoogleLLMVariant |
@@ -2013,3 +2015,77 @@ async def test_finalize_dynamic_instructions_with_static_instruction(): |
2013 | 2015 | assert llm_request.contents[0].parts[0].text == 'dynamic 1\n\ndynamic 2' |
2014 | 2016 | assert llm_request.contents[1].role == 'user' |
2015 | 2017 | assert llm_request.contents[1].parts[0].text == 'user question' |
| 2018 | + |
| 2019 | + |
| 2020 | +@pytest.mark.asyncio |
| 2021 | +async def test_resume_short_circuit_skips_partial_function_call(): |
| 2022 | + """A partial function_call at events[-1] must not drive the resume replay. |
| 2023 | +
|
| 2024 | + Partial events are SSE-display-only and never persisted to the session |
| 2025 | + (runners.py, invocation_context.py). If one leaks to events[-1] on a |
| 2026 | + resumable invocation, the resume short-circuit must ignore it and call the |
| 2027 | + LLM normally instead of re-executing it as a transfer. |
| 2028 | + """ |
| 2029 | + sub_agent = Agent( |
| 2030 | + name='sub_agent', |
| 2031 | + model=testing_utils.MockModel.create(responses=['unused']), |
| 2032 | + ) |
| 2033 | + root_agent = Agent( |
| 2034 | + name='root_agent', |
| 2035 | + model=testing_utils.MockModel.create(responses=['llm called']), |
| 2036 | + sub_agents=[sub_agent], |
| 2037 | + ) |
| 2038 | + |
| 2039 | + session_service = InMemorySessionService() |
| 2040 | + session = await session_service.create_session( |
| 2041 | + app_name='test_app', user_id='test_user' |
| 2042 | + ) |
| 2043 | + await session_service.append_event( |
| 2044 | + session, |
| 2045 | + Event( |
| 2046 | + invocation_id='i', |
| 2047 | + branch='root_agent', |
| 2048 | + author='user', |
| 2049 | + content=types.Content( |
| 2050 | + role='user', parts=[types.Part.from_text(text='go')] |
| 2051 | + ), |
| 2052 | + ), |
| 2053 | + ) |
| 2054 | + # A consumer leaked a partial streaming transfer call into the session view; |
| 2055 | + # stock ADK filters these, a buggy consumer may not. |
| 2056 | + session.events.append( |
| 2057 | + Event( |
| 2058 | + invocation_id='i', |
| 2059 | + branch='root_agent', |
| 2060 | + author='root_agent', |
| 2061 | + partial=True, |
| 2062 | + content=types.Content( |
| 2063 | + role='model', |
| 2064 | + parts=[ |
| 2065 | + types.Part.from_function_call( |
| 2066 | + name='transfer_to_agent', args={'agent_name': 'sub_agent'} |
| 2067 | + ) |
| 2068 | + ], |
| 2069 | + ), |
| 2070 | + ) |
| 2071 | + ) |
| 2072 | + |
| 2073 | + invocation_context = InvocationContext( |
| 2074 | + session_service=session_service, |
| 2075 | + invocation_id='i', |
| 2076 | + agent=root_agent, |
| 2077 | + session=session, |
| 2078 | + run_config=RunConfig(), |
| 2079 | + resumability_config=ResumabilityConfig(is_resumable=True), |
| 2080 | + branch='root_agent', |
| 2081 | + ) |
| 2082 | + |
| 2083 | + events = [ |
| 2084 | + event |
| 2085 | + async for event in root_agent._llm_flow.run_async(invocation_context) |
| 2086 | + ] |
| 2087 | + |
| 2088 | + # The LLM was called once (short-circuit skipped) and the partial call was |
| 2089 | + # not re-executed as a transfer. |
| 2090 | + assert root_agent.model.response_index == 0 |
| 2091 | + assert not any(e.actions and e.actions.transfer_to_agent for e in events) |
0 commit comments