🔴 Required Information
Describe the Bug:
bidi_stream_query() silently drops session state from the first queue message. The state field is never read from first_request, and when no session_id is provided, async_create_session(user_id=user_id) is called without the state= parameter — creating a stateless session. Tools that read ToolContext.state receive an empty dict.
The same state flows correctly through async_stream_query() when the session is pre-created with async_create_session(state=...).
This blocks any application that needs to inject client-side state (user profile, page context, preferences) into a live/bidi streaming session.
Steps to Reproduce:
- Create
app/agent.py:
importjsonfromgoogle.adk.agentsimportAgentfromgoogle.adk.toolsimportFunctionTool, ToolContextfromgoogle.genai.typesimportGenerateContentConfigdefget_name(tool_context: ToolContext) ->str:
"""Retrieve the user's name from session state."""name=tool_context.state.get("name", "NO_NAME_IN_STATE")
returnjson.dumps({"name": name})
root_agent=Agent(
model="gemini-2.5-flash",
name="state_test_agent",
instruction=(
"You are a test agent. When the user says hello, ALWAYS call get_name first. ""If it returns a real name, respond with 'Hello' followed by that name. ""If it returns 'NO_NAME_IN_STATE', respond 'Hello stranger!'"
),
tools=[FunctionTool(get_name)],
generate_content_config=GenerateContentConfig(temperature=0.0),
)- Create
app/agent_engine_app.py:
fromvertexai.preview.reasoning_engines.templates.adkimportAdkAppfromapp.agentimportroot_agentclassAgentEngineApp(AdkApp):
defregister_operations(self):
operations=super().register_operations()
operations["bidi_stream"] = ["bidi_stream_query"]
operations["async_stream"] = ["async_stream_query"]
returnoperationsagent_engine=AgentEngineApp(agent=root_agent)
- Run the following test that instruments
async_create_session to prove state is dropped:
importasynciofromapp.agent_engine_appimportagent_enginedefextract_text(event):
content=event.get("content", {})
ifisinstance(content, dict):
forpartincontent.get("parts", []):
ifisinstance(part, dict) andpart.get("text"):
returnpart["text"]
return""asyncdefmain():
# TEST 1: async_stream_query — state flows correctlysession=awaitagent_engine.async_create_session(
user_id="async-user", state={"name": "Alice"}
)
response=""asyncforeventinagent_engine.async_stream_query(
user_id="async-user",
session_id=session.id,
message={"role": "user", "parts": [{"text": "Hello"}]},
):
response+=extract_text(event)
print(f"async_stream_query response: {response}")
print(f" 'Alice' in response: {'alice'inresponse.lower()}") # True# TEST 2: bidi_stream_query — instrument to prove state is droppedcaptured_kwargs= []
original=agent_engine.async_create_sessionasyncdefinstrumented(*args, **kwargs):
captured_kwargs.append(kwargs)
returnawaitoriginal(*args, **kwargs)
agent_engine.async_create_session=instrumentedqueue=asyncio.Queue()
awaitqueue.put({
"user_id": "bidi-user",
"state": {"name": "Charlie"}, # THIS IS SILENTLY DROPPED
})
awaitqueue.put({
"content": {"role": "user", "parts": [{"text": "Hello"}]},
})
try:
asyncwithasyncio.timeout(10):
asyncforeventinagent_engine.bidi_stream_query(queue):
passexceptException:
pass# Expected: Live API model error (irrelevant to the bug)finally:
agent_engine.async_create_session=originalprint(f"\nbidi_stream_query called async_create_session with:")
forkwincaptured_kwargs:
print(f" kwargs = {kw}")
state_passed=any(kw.get("state") forkwincaptured_kwargs)
print(f" state= parameter present: {state_passed}") # False — BUGasyncio.run(main())- Output:
async_stream_query response: Hello Alice!
'Alice' in response: True
bidi_stream_query called async_create_session with:
kwargs = {'user_id': 'bidi-user'}
state= parameter present: False
Expected Behavior:
bidi_stream_query() should read state from the first queue message and pass it to async_create_session(). The get_name tool should return "Charlie" via ToolContext.state, and the agent should respond with "Hello Charlie!".
Observed Behavior:
bidi_stream_query() ignores the state field in the first queue message. async_create_session() is called with only user_id= — no state= parameter. The session is created stateless. ToolContext.state is empty. The agent responds with "Hello stranger!" instead of "Hello Charlie!".
The state field is visible in the queue message dict but is never read at adk.py:1170-1176.
Environment Details:
- ADK Library Version: 1.27.1 (also verified in source: v1.28.0
live_request_queue.py and aiplatform v1.145.0 adk.py — both unfixed) - Desktop OS: macOS 15.4
- Python Version: 3.13.0
Model Information:
- Are you using LiteLLM: No
- Which model:
gemini-2.5-flash (for async path), gemini-live-2.5-flash-native-audio (for bidi/live path)
🟡 Optional Information
Regression:
This has likely never worked — bidi_stream_query() has never read state from the first queue message. The state parameter was added to async_create_session() but the bidi path was not updated to use it.
How often has this issue occurred?:
Always (100%) — deterministic. State is never read from the first bidi message.
Minimal Reproduction Code:
See Steps to Reproduce above. The key proof is instrumenting async_create_session:
# What bidi_stream_query passes:async_create_session(user_id='bidi-user')
# No state= parameter# What it should pass:async_create_session(user_id='bidi-user', state={'name': 'Charlie'})Root Cause:
Two issues in the ADK/vertexai stack:
1. bidi_stream_query() never reads state from first queue message
vertexai/preview/reasoning_engines/templates/adk.py (lines ~1169-1182 in v1.141.0):
first_request=awaitrequest_queue.get()
user_id=first_request.get("user_id") # readsession_id=first_request.get("session_id") # readrun_config=first_request.get("run_config") # readfirst_live_request=first_request.get("live_request") # read# state = first_request.get("state") # NEVER READifnotsession_id:
session=awaitself.async_create_session(user_id=user_id) # no state=session_id=session.idasync_create_session() at line ~1350 accepts state: Optional[Dict[str, Any]] = None, but bidi_stream_query never passes it.
2. LiveRequest has no state field for subsequent messages
google/adk/agents/live_request_queue.py (lines 26-57):
classLiveRequest(BaseModel):
content: Optional[types.Content] =Noneblob: Optional[types.Blob] =Noneactivity_start: Optional[types.ActivityStart] =Noneactivity_end: Optional[types.ActivityEnd] =Noneclose: bool=False# NO state field — Pydantic silently drops unknown fields
_forward_requests() at line ~1194 calls LiveRequest.model_validate(request), which drops any state or state_delta key via Pydantic validation.
Suggested Fix:
3-line change in bidi_stream_query():
first_request=awaitrequest_queue.get()
user_id=first_request.get("user_id")
session_id=first_request.get("session_id")
state=first_request.get("state") # ← ADD: read state from first messagerun_config=first_request.get("run_config")
first_live_request=first_request.get("live_request")
ifnotsession_id:
session=awaitself.async_create_session(
user_id=user_id,
state=state, # ← ADD: pass state to session creation
)
session_id=session.idFor the case where session_id IS provided but state needs to be updated, a state_delta mechanism on LiveRequest would also be needed — but the above fix addresses the most common pattern (new session with initial state).
Related Issues:
🔴 Required Information
Describe the Bug:
bidi_stream_query()silently drops session state from the first queue message. Thestatefield is never read fromfirst_request, and when nosession_idis provided,async_create_session(user_id=user_id)is called without thestate=parameter — creating a stateless session. Tools that readToolContext.statereceive an empty dict.The same state flows correctly through
async_stream_query()when the session is pre-created withasync_create_session(state=...).This blocks any application that needs to inject client-side state (user profile, page context, preferences) into a live/bidi streaming session.
Steps to Reproduce:
app/agent.py:app/agent_engine_app.py:async_create_sessionto prove state is dropped:Expected Behavior:
bidi_stream_query()should readstatefrom the first queue message and pass it toasync_create_session(). Theget_nametool should return"Charlie"viaToolContext.state, and the agent should respond with "Hello Charlie!".Observed Behavior:
bidi_stream_query()ignores thestatefield in the first queue message.async_create_session()is called with onlyuser_id=— nostate=parameter. The session is created stateless.ToolContext.stateis empty. The agent responds with "Hello stranger!" instead of "Hello Charlie!".The
statefield is visible in the queue message dict but is never read atadk.py:1170-1176.Environment Details:
live_request_queue.pyand aiplatform v1.145.0adk.py— both unfixed)Model Information:
gemini-2.5-flash(for async path),gemini-live-2.5-flash-native-audio(for bidi/live path)🟡 Optional Information
Regression:
This has likely never worked —
bidi_stream_query()has never readstatefrom the first queue message. Thestateparameter was added toasync_create_session()but the bidi path was not updated to use it.How often has this issue occurred?:
Always (100%) — deterministic. State is never read from the first bidi message.
Minimal Reproduction Code:
See Steps to Reproduce above. The key proof is instrumenting
async_create_session:Root Cause:
Two issues in the ADK/vertexai stack:
1.
bidi_stream_query()never readsstatefrom first queue messagevertexai/preview/reasoning_engines/templates/adk.py(lines ~1169-1182 in v1.141.0):async_create_session()at line ~1350 acceptsstate: Optional[Dict[str, Any]] = None, butbidi_stream_querynever passes it.2.
LiveRequesthas no state field for subsequent messagesgoogle/adk/agents/live_request_queue.py(lines 26-57):_forward_requests()at line ~1194 callsLiveRequest.model_validate(request), which drops anystateorstate_deltakey via Pydantic validation.Suggested Fix:
3-line change in
bidi_stream_query():For the case where
session_idIS provided but state needs to be updated, astate_deltamechanism onLiveRequestwould also be needed — but the above fix addresses the most common pattern (new session with initial state).Related Issues:
run_live(): Fire-and-forget tool calls cause duplicate model responses via orphaned function response reinjection #4902 — [Live] Fire-and-forget tool calls cause duplicate responses (our previous report, same project)session_id— notstate)