🔴 Required Information
Describe the Bug:
When triggering an agent via the /run or /run_sse API endpoints, clients can provide a state_delta payload to initialize or update the session state before the agent executes.
This works correctly for traditional agents running through _run_with_trace. However, it is silently ignored when the agent uses the modern Workflow/NodeRunner architecture. The state_delta gets dropped during the transition from run_async to _run_node_async, meaning nodes never receive the intended initial state.
Steps to Reproduce:
- Start the ADK web server.
- Make a POST request to
/run (or /run_sse), providing both new_message and a state_delta for an LlmAgent/Agent:
{
"appName": "normal_agent",
"userId": "user",
"sessionId": "test_state_delta",
"newMessage": {
"parts": [{"text": "test message state delta"}],
"role": "user"
},
"streaming": false,
"stateDelta": {
"test_state": "must_change"
}
}
- Observe that the agent executes, but the session state remains empty (
{}).
Expected Behavior:
The state_delta provided in the API request should be applied to the session.state (and persisted as an EventActions payload on the initial user event) BEFORE the workflow starts executing, making it immediately available to all nodes via ctx.state.
Observed Behavior:
The state_delta is silently dropped.
Environment Details:
- ADK Library Version:
2.0.0-beta.1
- Desktop OS: Windows / macOS / Linux
- Python Version: Python 3.10+
Model Information:
- Are you using LiteLLM: Yes
- Which model is being used: gemini-3-flash-preview
🟡 Optional Information
Regression:
Yes. This is a severe regression introduced in version 2.0.0-beta.1 (specifically during the transition of LlmAgent to the new NodeRunner architecture).
- In version
v1.34.0 (and 1.33.0), calling /run with a state_delta for a normal Agent (which is a TypeAlias for LlmAgent) worked perfectly. This is because in older versions, LlmAgent ran through the traditional _run_with_trace execution path, which correctly applies state_delta.
- In
v2.0.0-beta.1, a hardcoded branch was introduced in runners.py (run_async):
if isinstance(self.agent, LlmAgent):
# ... forces mode='chat' and routes to NodeRunner ...
self._run_node_async(...)
By forcing all LlmAgent instances into the new _run_node_async path (which completely lacks state_delta support), backward compatibility for session initialization was broken.
Additional Context (Code Trace):
I have traced the exact point where the data is dropped in src/google/adk/runners.py:
- The API endpoint correctly passes
state_delta to runner.run_async(...) (Line 775).
run_async branches based on agent type:
- Traditional Path: Passes
state_delta through _setup_context_for_new_invocation -> _handle_new_message -> _append_new_message_to_session, which successfully appends an Event(actions=EventActions(state_delta=state_delta)).
- Workflow Path: For
LlmAgent and BaseNode, it delegates to self._run_node_async().
- The Gap: The signature for
_run_node_async (Line 413) does not accept a state_delta parameter. The callers simply omit it.
- Furthermore, inside
_run_node_async, it calls _append_user_event (Line 577), which currently has no logic to handle state_delta or attach it to the EventActions, unlike its traditional counterpart _append_new_message_to_session.
Proposed Fix:
- Add
state_delta: Optional[dict[str, Any]] = None to the signature of _run_node_async.
- Update
_append_user_event to accept state_delta and construct the event with EventActions(state_delta=state_delta) if present (matching the logic in _append_new_message_to_session).
- Pass
state_delta=state_delta in the call sites to _run_node_async inside run_async.
Minimal Reproduction Code:
1. Agent Code (normal_agent):
from google.adk.agents import Agent
from google.adk.models import LiteLlm
from google.adk.agents.callback_context import CallbackContext
import logging
logger = logging.getLogger(__name__)
def show_state(callback_context: CallbackContext):
logger.info(f"[DEBUG] current events: {callback_context.session.events}")
# Define the chat agent
root_agent = Agent(
name="chat_agent",
model=LiteLlm(model="litellm_proxy/gemini-3-flash-preview"),
instruction="You are a helpful assistant.",
before_agent_callback=[show_state],
)
2. Curl Request to /run:
curl --request POST \
--url http://localhost:8000/run \
--header 'authorization: Bearer sk-123' \
--header 'content-type: application/json' \
--data '{
"appName": "normal_agent",
"userId": "user",
"sessionId": "test_state_delta",
"newMessage": {
"parts": [{"text": "test message state delta"}],
"role": "user"
},
"streaming": false,
"stateDelta": {
"test_state": "must_change"
}
}'
3. Logs in v1.33.0 (Working correctly):
2026-05-20 14:27:59.916 | INFO | normal_agent.agent | [DEBUG] current events: [Event(model_version=None, content=Content(
parts=[
Part(
text='test message state delta'
),
],
role='string'
), grounding_metadata=None, partial=None, turn_complete=None, finish_reason=None, error_code=None, error_message=None, interrupted=None, custom_metadata=None, usage_metadata=None, live_session_resumption_update=None, live_session_id=None, go_away=None, input_transcription=None, output_transcription=None, avg_logprobs=None, logprobs_result=None, cache_metadata=None, citation_metadata=None, interaction_id=None, invocation_id='e-ab863237-b161-4f7d-a1ff-559c82c0ec1a', author='user', actions=EventActions(skip_summarization=None, state_delta={'test_state': 'must_change'}, artifact_delta={}, transfer_to_agent=None, escalate=None, requested_auth_configs={}, requested_tool_confirmations={}, compaction=None, end_of_agent=None, agent_state=None, rewind_before_invocation_id=None, render_ui_widgets=None), long_running_tool_ids=None, branch=None, id='69d7e17f-48d6-46c3-9090-1c0cc37b9135', timestamp=1779262079.8746789)]
4. Logs in v2.0.0-beta.1 (Regression - state is empty):
2026-05-20 14:40:47.104 | INFO | normal_agent.agent | [DEBUG] current events: [Event(model_version=None, content=Content(
parts=[
Part(
text='test message state delta'
),
],
role='string'
), grounding_metadata=None, partial=None, turn_complete=None, finish_reason=None, error_code=None, error_message=None, interrupted=None, custom_metadata=None, usage_metadata=None, live_session_resumption_update=None, live_session_id=None, go_away=None, input_transcription=None, output_transcription=None, avg_logprobs=None, logprobs_result=None, cache_metadata=None, citation_metadata=None, interaction_id=None, invocation_id='e-60553fe6-5e11-4639-aac2-a4ca41086e05', author='user', actions=EventActions(skip_summarization=None, state_delta={}, artifact_delta={}, transfer_to_agent=None, escalate=None, requested_auth_configs={}, requested_tool_confirmations={}, compaction=None, end_of_agent=None, agent_state=None, rewind_before_invocation_id=None, route=None, request_task=None, finish_task=None, render_ui_widgets=None, set_model_response=None), output=None, node_info=NodeInfo(path='', output_for=None, message_as_output=None), long_running_tool_ids=None, branch=None, id='ff8512e8-19b6-42e7-a6b4-c36110a3d7ab', timestamp=1779262839.6619968)]
How often has this issue occurred?:
🔴 Required Information
Describe the Bug:
When triggering an agent via the
/runor/run_sseAPI endpoints, clients can provide astate_deltapayload to initialize or update the session state before the agent executes.This works correctly for traditional agents running through
_run_with_trace. However, it is silently ignored when the agent uses the modern Workflow/NodeRunner architecture. Thestate_deltagets dropped during the transition fromrun_asyncto_run_node_async, meaning nodes never receive the intended initial state.Steps to Reproduce:
/run(or/run_sse), providing bothnew_messageand astate_deltafor an LlmAgent/Agent:{ "appName": "normal_agent", "userId": "user", "sessionId": "test_state_delta", "newMessage": { "parts": [{"text": "test message state delta"}], "role": "user" }, "streaming": false, "stateDelta": { "test_state": "must_change" } }{}).Expected Behavior:
The
state_deltaprovided in the API request should be applied to thesession.state(and persisted as anEventActionspayload on the initial user event) BEFORE the workflow starts executing, making it immediately available to all nodes viactx.state.Observed Behavior:
The
state_deltais silently dropped.Environment Details:
2.0.0-beta.1Model Information:
🟡 Optional Information
Regression:
Yes. This is a severe regression introduced in version
2.0.0-beta.1(specifically during the transition ofLlmAgentto the new NodeRunner architecture).v1.34.0(and1.33.0), calling/runwith astate_deltafor a normalAgent(which is a TypeAlias forLlmAgent) worked perfectly. This is because in older versions,LlmAgentran through the traditional_run_with_traceexecution path, which correctly appliesstate_delta.v2.0.0-beta.1, a hardcoded branch was introduced inrunners.py(run_async):LlmAgentinstances into the new_run_node_asyncpath (which completely lacksstate_deltasupport), backward compatibility for session initialization was broken.Additional Context (Code Trace):
I have traced the exact point where the data is dropped in
src/google/adk/runners.py:state_deltatorunner.run_async(...)(Line 775).run_asyncbranches based on agent type:state_deltathrough_setup_context_for_new_invocation->_handle_new_message->_append_new_message_to_session, which successfully appends anEvent(actions=EventActions(state_delta=state_delta)).LlmAgentandBaseNode, it delegates toself._run_node_async()._run_node_async(Line 413) does not accept astate_deltaparameter. The callers simply omit it._run_node_async, it calls_append_user_event(Line 577), which currently has no logic to handlestate_deltaor attach it to theEventActions, unlike its traditional counterpart_append_new_message_to_session.Proposed Fix:
state_delta: Optional[dict[str, Any]] = Noneto the signature of_run_node_async._append_user_eventto acceptstate_deltaand construct the event withEventActions(state_delta=state_delta)if present (matching the logic in_append_new_message_to_session).state_delta=state_deltain the call sites to_run_node_asyncinsiderun_async.Minimal Reproduction Code:
1. Agent Code (
normal_agent):2. Curl Request to
/run:3. Logs in v1.33.0 (Working correctly):
4. Logs in v2.0.0-beta.1 (Regression - state is empty):
How often has this issue occurred?: