Skip to content

Commit c328cec

Browse files
GWealecopybara-github
authored andcommitted
fix: assemble only the current turn
The Interactions adapter already sends only the current turn when a previous interaction id is present, but the standard flow still walked and copied the whole session history first, and could invoke the explicit cache manager even though Interactions only supports implicit caching. This finds the previous interaction id up front, assembles just the current turn for those requests, and skips explicit cache mutation on that path. Ordinary GenerateContent requests keep full history. Co-authored-by: George Weale <gweale@google.com> PiperOrigin-RevId: 952369834
1 parent ebaef9f commit c328cec

7 files changed

Lines changed: 103 additions & 8 deletions

File tree

src/google/adk/flows/llm_flows/contents.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,10 @@ async def run_async(
9090
)
9191

9292
is_single_turn = getattr(agent, 'mode', None) == 'single_turn'
93-
if agent.include_contents == 'default':
93+
if (
94+
agent.include_contents == 'default'
95+
and not llm_request.previous_interaction_id
96+
):
9497
# Include full conversation history
9598
llm_request.contents = _get_contents(
9699
invocation_context.branch,
@@ -103,7 +106,8 @@ async def run_async(
103106
include_thoughts_from_other_agents=include_thoughts_from_other_agents,
104107
)
105108
else:
106-
# Include current turn context only (no conversation history)
109+
# Include current turn context only (no conversation history). Stateful
110+
# Interactions requests already retain earlier turns server-side.
107111
llm_request.contents = _get_current_turn_contents(
108112
invocation_context.branch,
109113
invocation_context.session.events,

src/google/adk/flows/llm_flows/interactions_processor.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ class InteractionsRequestProcessor(BaseLlmRequestProcessor):
8585
This processor extracts the previous_interaction_id from session events
8686
to enable stateful conversation chaining via the Interactions API.
8787
The actual content filtering (retaining only latest user messages) is
88-
done in the Gemini class when using the Interactions API.
88+
done by the content request processor after this processor runs.
8989
"""
9090

9191
async def run_async(

src/google/adk/flows/llm_flows/single_flow.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,13 +52,13 @@ def _create_request_processors() -> list[BaseLlmRequestProcessor]:
5252
# Compaction should run before contents so compacted events are reflected
5353
# in the model request context.
5454
compaction.request_processor,
55+
# Extract the Interactions chain id before contents. Chained requests
56+
# only need the current turn because the service retains prior state.
57+
interactions_processor.request_processor,
5558
contents.request_processor,
5659
# Context cache processor sets up cache config and finds
5760
# existing cache metadata.
5861
context_cache_processor.request_processor,
59-
# Interactions processor extracts previous_interaction_id for
60-
# stateful conversations via the Interactions API.
61-
interactions_processor.request_processor,
6262
# Some implementations of NL Planning mark planning contents
6363
# as thoughts in the post processor. Since these need to be
6464
# unmarked, NL Planning should be after contents.

src/google/adk/models/google_llm.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ async def generate_content_async(
200200
# Handle context caching if configured
201201
cache_metadata = None
202202
cache_manager = None
203-
if llm_request.cache_config:
203+
if llm_request.cache_config and not self.use_interactions_api:
204204
from ..telemetry.tracing import tracer
205205
from .gemini_context_cache_manager import GeminiContextCacheManager
206206

tests/unittests/flows/llm_flows/test_contents.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,44 @@ async def test_include_contents_default_full_history():
8888
]
8989

9090

91+
@pytest.mark.asyncio
92+
async def test_chained_interactions_builds_only_current_turn_contents():
93+
"""Stateful Interactions requests do not copy history the server retains."""
94+
agent = Agent(
95+
model="gemini-2.5-flash", name="test_agent", include_contents="default"
96+
)
97+
llm_request = LlmRequest(
98+
model="gemini-2.5-flash", previous_interaction_id="interaction-1"
99+
)
100+
invocation_context = await testing_utils.create_invocation_context(
101+
agent=agent
102+
)
103+
invocation_context.session.events = [
104+
Event(
105+
invocation_id="inv1",
106+
author="user",
107+
content=types.UserContent("Historical message"),
108+
),
109+
Event(
110+
invocation_id="inv2",
111+
author="test_agent",
112+
content=types.ModelContent("Historical response"),
113+
),
114+
Event(
115+
invocation_id="inv3",
116+
author="user",
117+
content=types.UserContent("Current message"),
118+
),
119+
]
120+
121+
async for _ in contents.request_processor.run_async(
122+
invocation_context, llm_request
123+
):
124+
pass
125+
126+
assert llm_request.contents == [types.UserContent("Current message")]
127+
128+
91129
@pytest.mark.asyncio
92130
async def test_include_contents_none_current_turn_only():
93131
"""Test that include_contents='none' includes only current turn context."""

tests/unittests/flows/llm_flows/test_interactions_processor.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,10 @@
1717
from unittest.mock import MagicMock
1818

1919
from google.adk.events.event import Event
20+
from google.adk.flows.llm_flows import contents
2021
from google.adk.flows.llm_flows import interactions_processor
22+
from google.adk.flows.llm_flows.single_flow import SingleFlow
2123
from google.genai import types
22-
import pytest
2324

2425

2526
class TestInteractionsRequestProcessor:
@@ -224,6 +225,18 @@ def test_is_event_in_branch_root_events_included(self):
224225
)
225226

226227

228+
def test_single_flow_extracts_interaction_state_before_contents():
229+
"""Chained requests expose their interaction ID to content assembly."""
230+
flow = SingleFlow()
231+
232+
interactions_index = flow.request_processors.index(
233+
interactions_processor.request_processor
234+
)
235+
contents_index = flow.request_processors.index(contents.request_processor)
236+
237+
assert interactions_index < contents_index
238+
239+
227240
def _evt(author: str, interaction_id: str | None, branch: str | None) -> Event:
228241
return Event(author=author, interaction_id=interaction_id, branch=branch)
229242

tests/unittests/models/test_google_llm.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2056,6 +2056,46 @@ async def mock_coro():
20562056
assert second_arg.invocations_used == cache_metadata.invocations_used
20572057

20582058

2059+
@pytest.mark.asyncio
2060+
async def test_interactions_api_does_not_apply_explicit_cache(llm_request):
2061+
"""Interactions requests use implicit caching without mutating the prompt."""
2062+
gemini = Gemini(model="gemini-2.5-flash", use_interactions_api=True)
2063+
llm_request.cache_config = ContextCacheConfig()
2064+
original_request = llm_request.model_copy(deep=True)
2065+
2066+
async def generate_via_interactions(_llm_request, _stream):
2067+
yield LlmResponse(
2068+
content=Content(
2069+
role="model", parts=[Part.from_text(text="interaction response")]
2070+
)
2071+
)
2072+
2073+
with (
2074+
mock.patch.object(gemini, "_preprocess_request", new=AsyncMock()),
2075+
mock.patch.object(
2076+
gemini,
2077+
"_generate_content_via_interactions",
2078+
new=generate_via_interactions,
2079+
),
2080+
mock.patch(
2081+
"google.adk.models.gemini_context_cache_manager.GeminiContextCacheManager"
2082+
) as cache_manager_class,
2083+
):
2084+
responses = [
2085+
response
2086+
async for response in gemini.generate_content_async(llm_request)
2087+
]
2088+
2089+
assert responses[0].content.parts[0].text == "interaction response"
2090+
assert llm_request.contents == original_request.contents
2091+
assert (
2092+
llm_request.config.system_instruction
2093+
== original_request.config.system_instruction
2094+
)
2095+
assert llm_request.config.cached_content is None
2096+
cache_manager_class.assert_not_called()
2097+
2098+
20592099
def test_build_function_declaration_log():
20602100
"""Test that _build_function_declaration_log formats function declarations correctly."""
20612101
# Test case 1: Function with parameters and response

0 commit comments

Comments
 (0)