Skip to content

[Live/bidi_stream_query] Session state silently dropped — first queue message 'state' field never read, async_create_session called without state= #5113

Description

@smwitkowski

🔴 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:

  1. 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),
)
  1. 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)
  1. 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())
  1. 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.id

async_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.id

For 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:

Metadata

Metadata

Assignees

Labels

agent engine[Component] This issue is related to Vertex AI Agent Enginelive[Component] This issue is related to live, voice and video chat

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions