🔴 Required Information
Describe the Bug:
When LlmAgent.static_instruction contains non-text content such as a PDF provided through file_data, ADK converts that part into user content.
On the first turn, the generated request has the expected order:
static PDF content
dynamic instruction
current user message
On the second and subsequent turns, however, the same static PDF content is inserted after the existing conversation history:
previous user message
previous model response
static PDF content
dynamic instruction
current user message
This means the supposedly static PDF is no longer part of a stable request prefix across turns.
Because Gemini implicit context caching relies on matching a sufficiently large prefix, this ordering appears to prevent a large PDF in static_instruction from benefiting from implicit context caching.
The PDF itself contains approximately 40,040 image tokens. Despite sending the same PDF on consecutive requests, neither response contains cached_content_token_count.
This is especially surprising because static_instruction is documented as being sent at the beginning of the request for context-caching purposes.
|
This field is primarily for context caching optimization. Static instructions |
|
are sent as system instruction at the beginning of the request, allowing |
|
for improved performance when the static portion remains unchanged. Live API |
Steps to Reproduce:
-
Install ADK 2.6.1.
pip install google-adk==2.6.1
-
Create the agent from the minimal reproduction below.
-
Configure the Vertex AI backend.
export GOOGLE_GENAI_USE_VERTEXAI=TRUE
export GOOGLE_CLOUD_PROJECT="your-project"
export GOOGLE_CLOUD_LOCATION="global"
-
Start the agent with debug logging enabled.
adk web -v >debug.log 2>&1
-
Start a new session and send:
-
In the same session, send:
-
Inspect the generated contents and usage_metadata in the debug log.
Expected Behavior:
Contents originating from static_instruction should remain at the beginning of the generated request on every turn. The dynamic instruction should follow the static contents, before the conversation history.
For example:
static PDF content
dynamic instruction
previous user message
previous model response
current user message
This preserves the documented ordering between static_instruction and instruction, while keeping the large static PDF as a stable request prefix.
Implicit cache hits are controlled by the model backend and are not guaranteed. However, ADK should preserve this stable prefix rather than moving static content behind changing conversation history.
Observed Behavior:
The first request is ordered as follows:
1. Referenced file data + PDF file_data
2. Dynamic instruction
3. Current user message: こんにちは
The second request is ordered as follows:
1. Previous user message: こんにちは
2. Previous model response
3. Referenced file data + the same PDF file_data
4. Dynamic instruction
5. Current user message: 論文の目次を教えて
Usage metadata for the first response:
{
"candidates_token_count": 109,
"prompt_token_count": 40259,
"prompt_tokens_details": [
{
"modality": "IMAGE",
"token_count": 40040
},
{
"modality": "TEXT",
"token_count": 219
}
],
"thoughts_token_count": 109,
"total_token_count": 40477,
"traffic_type": "ON_DEMAND"
}
Usage metadata for the second response:
{
"candidates_token_count": 1068,
"prompt_token_count": 40483,
"prompt_tokens_details": [
{
"modality": "TEXT",
"token_count": 443
},
{
"modality": "IMAGE",
"token_count": 40040
}
],
"thoughts_token_count": 852,
"total_token_count": 42403,
"traffic_type": "ON_DEMAND"
}
Neither response reports cached_content_token_count.
Environment Details:
- ADK Library Version (
pip show google-adk): 2.6.1
- Desktop OS: macOS
- Python Version: 3.13.12
Model Information:
- Are you using LiteLLM: No
- Which model are you using:
gemini-3.6-flash
- Backend: Vertex AI
🟡 Optional Information
Regression:
Unknown.
I have not verified an earlier ADK version in which non-text static_instruction content remained at the beginning of multi-turn requests.
Logs:
The relevant request ordering from the debug log is summarized above. The important difference is that the same PDF moves from the beginning of the first request to a position after the previous model response in the second request.
Full: https://gist.github.com/ftnext/c5e79dc8b19d6e59298633e4dd866c86
Screenshots / Video:
N/A
Additional Context:
The relevant flow appears to be:
-
Non-text parts of static_instruction are extracted into user contents by
LlmRequest.append_instructions():
llm_request.py
-
The contents processor rebuilds the conversation history and then inserts
all instruction-related user contents:
contents.py
-
_add_instructions_to_user_content() inserts those contents before the last
continuous batch of user contents rather than at position zero:
contents.py
This placement is appropriate for dynamic instructions, but non-text contents originating from static_instruction appear to need separate placement so that they remain a stable prefix.
Related issues and PRs:
Those appear related but do not address this issue: the present problem is the actual GenerateContentRequest.contents ordering for non-text static instructions and its effect on provider-side implicit caching.
Minimal Reproduction Code:
https://github.com/ftnext/agent-practice/tree/5a09ef5daf16cf27a54144daa89d2529e864cd5c/adk/pdf/static_instruction
based on https://github.com/google/adk-python/tree/v2.6.1/contributing/samples/multimodal/static_non_text_content
Proposed Regression Test:
The following test can be added to tests/unittests/flows/llm_flows/test_instructions.py.
It verifies request structure rather than asserting an actual cache hit, since implicit cache hits are controlled by the backend.
diff --git a/tests/unittests/flows/llm_flows/test_instructions.py b/tests/unittests/flows/llm_flows/test_instructions.py
--- a/tests/unittests/flows/llm_flows/test_instructions.py
+++ b/tests/unittests/flows/llm_flows/test_instructions.py
@@
from google.adk.agents.run_config import RunConfig
+from google.adk.events.event import Event
from google.adk.flows.llm_flows import instructions
@@
+@pytest.mark.asyncio
+async def test_static_instruction_file_precedes_multi_turn_history():
+ """Test that a static file remains a stable prefix across turns."""
+ file_uri = "gs://test-bucket/reference.pdf"
+ agent = LlmAgent(
+ name="test_agent",
+ instruction="Dynamic instruction",
+ static_instruction=types.Content(
+ parts=[
+ types.Part(
+ file_data=types.FileData(
+ file_uri=file_uri,
+ mime_type="application/pdf",
+ )
+ )
+ ]
+ ),
+ )
+ invocation_context = await _create_invocation_context(agent)
+ invocation_context.session.events = [
+ Event(
+ invocation_id="inv1",
+ author="user",
+ content=types.UserContent("First message"),
+ ),
+ Event(
+ invocation_id="inv2",
+ author="test_agent",
+ content=types.ModelContent("First response"),
+ ),
+ Event(
+ invocation_id="inv3",
+ author="user",
+ content=types.UserContent("Second message"),
+ ),
+ ]
+ llm_request = LlmRequest()
+
+ async for _ in request_processor.run_async(
+ invocation_context, llm_request
+ ):
+ pass
+ async for _ in contents_processor.run_async(
+ invocation_context, llm_request
+ ):
+ pass
+
+ assert len(llm_request.contents) == 5
+
+ static_content = llm_request.contents[0]
+ assert static_content.role == "user"
+ assert static_content.parts[0].text == (
+ "Referenced file data: file_data_0"
+ )
+ assert static_content.parts[1].file_data
+ assert static_content.parts[1].file_data.file_uri == file_uri
+
+ dynamic_content = llm_request.contents[1]
+ assert dynamic_content.role == "user"
+ assert dynamic_content.parts[0].text == "Dynamic instruction"
+
+ assert llm_request.contents[2] == types.UserContent("First message")
+ assert llm_request.contents[3] == types.ModelContent("First response")
+ assert llm_request.contents[4] == types.UserContent("Second message")
How often does this issue occur?
The request reordering occurs deterministically on the second and subsequent turns when conversation history contains a model response.
In the observed two-turn reproduction, no implicit cache hit was reported. The structural ordering issue occurs consistently, although an implicit cache hit itself is not guaranteed by the backend.
🔴 Required Information
Describe the Bug:
When
LlmAgent.static_instructioncontains non-text content such as a PDF provided throughfile_data, ADK converts that part into user content.On the first turn, the generated request has the expected order:
On the second and subsequent turns, however, the same static PDF content is inserted after the existing conversation history:
This means the supposedly static PDF is no longer part of a stable request prefix across turns.
Because Gemini implicit context caching relies on matching a sufficiently large prefix, this ordering appears to prevent a large PDF in
static_instructionfrom benefiting from implicit context caching.The PDF itself contains approximately 40,040 image tokens. Despite sending the same PDF on consecutive requests, neither response contains
cached_content_token_count.This is especially surprising because
static_instructionis documented as being sent at the beginning of the request for context-caching purposes.adk-python/src/google/adk/agents/llm_agent.py
Lines 284 to 286 in 740582e
Steps to Reproduce:
Install ADK 2.6.1.
Create the agent from the minimal reproduction below.
Configure the Vertex AI backend.
Start the agent with debug logging enabled.
Start a new session and send:
In the same session, send:
Inspect the generated
contentsandusage_metadatain the debug log.Expected Behavior:
Contents originating from
static_instructionshould remain at the beginning of the generated request on every turn. The dynamicinstructionshould follow the static contents, before the conversation history.For example:
This preserves the documented ordering between
static_instructionandinstruction, while keeping the large static PDF as a stable request prefix.Implicit cache hits are controlled by the model backend and are not guaranteed. However, ADK should preserve this stable prefix rather than moving static content behind changing conversation history.
Observed Behavior:
The first request is ordered as follows:
The second request is ordered as follows:
Usage metadata for the first response:
{ "candidates_token_count": 109, "prompt_token_count": 40259, "prompt_tokens_details": [ { "modality": "IMAGE", "token_count": 40040 }, { "modality": "TEXT", "token_count": 219 } ], "thoughts_token_count": 109, "total_token_count": 40477, "traffic_type": "ON_DEMAND" }Usage metadata for the second response:
{ "candidates_token_count": 1068, "prompt_token_count": 40483, "prompt_tokens_details": [ { "modality": "TEXT", "token_count": 443 }, { "modality": "IMAGE", "token_count": 40040 } ], "thoughts_token_count": 852, "total_token_count": 42403, "traffic_type": "ON_DEMAND" }Neither response reports
cached_content_token_count.Environment Details:
pip show google-adk): 2.6.1Model Information:
gemini-3.6-flash🟡 Optional Information
Regression:
Unknown.
I have not verified an earlier ADK version in which non-text
static_instructioncontent remained at the beginning of multi-turn requests.Logs:
The relevant request ordering from the debug log is summarized above. The important difference is that the same PDF moves from the beginning of the first request to a position after the previous model response in the second request.
Full: https://gist.github.com/ftnext/c5e79dc8b19d6e59298633e4dd866c86
Screenshots / Video:
N/A
Additional Context:
The relevant flow appears to be:
Non-text parts of
static_instructionare extracted into user contents byLlmRequest.append_instructions():llm_request.pyThe contents processor rebuilds the conversation history and then inserts
all instruction-related user contents:
contents.py_add_instructions_to_user_content()inserts those contents before the lastcontinuous batch of user contents rather than at position zero:
contents.pyThis placement is appropriate for dynamic instructions, but non-text contents originating from
static_instructionappear to need separate placement so that they remain a stable prefix.Related issues and PRs:
Those appear related but do not address this issue: the present problem is the actual
GenerateContentRequest.contentsordering for non-text static instructions and its effect on provider-side implicit caching.Minimal Reproduction Code:
https://github.com/ftnext/agent-practice/tree/5a09ef5daf16cf27a54144daa89d2529e864cd5c/adk/pdf/static_instruction
based on https://github.com/google/adk-python/tree/v2.6.1/contributing/samples/multimodal/static_non_text_content
Proposed Regression Test:
The following test can be added to
tests/unittests/flows/llm_flows/test_instructions.py.It verifies request structure rather than asserting an actual cache hit, since implicit cache hits are controlled by the backend.
How often does this issue occur?
The request reordering occurs deterministically on the second and subsequent turns when conversation history contains a model response.
In the observed two-turn reproduction, no implicit cache hit was reported. The structural ordering issue occurs consistently, although an implicit cache hit itself is not guaranteed by the backend.