Repository navigation
state_delta passed to /run endpoint is silently ignored in Workflow (NodeRunner) path聽#5763
Description
馃敶 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 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" } } - 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(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. - In
v2.0.0-beta.1, a hardcoded branch was introduced inrunners.py(run_async):By forcing allif isinstance(self.agent, LlmAgent): # ... forces mode='chat' and routes to NodeRunner ... self._run_node_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:
- The API endpoint correctly passes
state_deltatorunner.run_async(...)(Line 775). run_asyncbranches based on agent type:- Traditional Path: Passes
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)). - Workflow Path: For
LlmAgentandBaseNode, it delegates toself._run_node_async().
- Traditional Path: Passes
- The Gap: The signature for
_run_node_async(Line 413) does not accept astate_deltaparameter. The callers simply omit it. - Furthermore, inside
_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:
- Add
state_delta: Optional[dict[str, Any]] = Noneto the signature of_run_node_async. - Update
_append_user_eventto acceptstate_deltaand construct the event withEventActions(state_delta=state_delta)if present (matching the logic in_append_new_message_to_session). - Pass
state_delta=state_deltain the call sites to_run_node_asyncinsiderun_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?:
- Always (100%)
Activity
trongthanht3 commentedon May 20, 2026
Hi team, I opened #5767 for this regression. It threads
state_deltathrough the NodeRunner path so/runand/run_sseapply the requested session state before a rootBaseNodeorLlmAgentruns. I verified it locally with unit tests, fullpytest,tox, and manual/run//run_ssechecks.fix(runners): Preserve state_delta in NodeRunner path
fix(runners): Preserve state_delta in NodeRunner path