From 6a929af718fa77199d1eecc62b16c54beb1c8d84 Mon Sep 17 00:00:00 2001 From: George Weale Date: Mon, 2 Mar 2026 09:25:00 -0800 Subject: [PATCH] fix: Prevent splitting of SSE events with artifactDelta for function resume requests When handling a /run_sse request that includes a functionCallEventId, do not split events that contain both content and artifactDelta Close #4487 Co-authored-by: George Weale PiperOrigin-RevId: 877435561 --- src/google/adk/cli/adk_web_server.py | 5 ++- tests/unittests/cli/test_fast_api.py | 51 ++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/src/google/adk/cli/adk_web_server.py b/src/google/adk/cli/adk_web_server.py index 469b33fe..afedb738 100644 --- a/src/google/adk/cli/adk_web_server.py +++ b/src/google/adk/cli/adk_web_server.py @@ -208,6 +208,8 @@ class RunAgentRequest(common.BaseModel): new_message: Optional[types.Content] = None streaming: bool = False state_delta: Optional[dict[str, Any]] = None + # for long-running function resume requests (e.g., OAuth callback) + function_call_event_id: Optional[str] = None # for resume long-running functions invocation_id: Optional[str] = None @@ -1707,7 +1709,8 @@ class AdkWebServer: # 2) a content-less "action-only" event carrying `artifactDelta` events_to_stream = [event] if ( - event.actions.artifact_delta + not req.function_call_event_id + and event.actions.artifact_delta and event.content and event.content.parts ): diff --git a/tests/unittests/cli/test_fast_api.py b/tests/unittests/cli/test_fast_api.py index d6ccf6e2..0ea28e66 100755 --- a/tests/unittests/cli/test_fast_api.py +++ b/tests/unittests/cli/test_fast_api.py @@ -1116,6 +1116,57 @@ def test_agent_run_sse_splits_artifact_delta( assert sse_events[1]["actions"]["artifactDelta"] == {"artifact.txt": 0} +def test_agent_run_sse_does_not_split_artifact_delta_for_function_resume( + test_app, create_test_session, monkeypatch +): + """Test /run_sse keeps artifactDelta with content for function resume flow.""" + info = create_test_session + + async def run_async_with_artifact_delta( + self, + *, + user_id: str, + session_id: str, + invocation_id: Optional[str] = None, + new_message: Optional[types.Content] = None, + state_delta: Optional[dict[str, Any]] = None, + run_config: Optional[RunConfig] = None, + ): + del user_id, session_id, invocation_id, new_message, state_delta, run_config + yield Event( + author="dummy agent", + invocation_id="invocation_id", + content=types.Content( + role="model", parts=[types.Part(text="LLM reply")] + ), + actions=EventActions(artifact_delta={"artifact.txt": 0}), + ) + + monkeypatch.setattr(Runner, "run_async", run_async_with_artifact_delta) + + payload = { + "app_name": info["app_name"], + "user_id": info["user_id"], + "session_id": info["session_id"], + "new_message": {"role": "user", "parts": [{"text": "Hello agent"}]}, + "streaming": True, + "functionCallEventId": "function-call-event-id", + } + + response = test_app.post("/run_sse", json=payload) + assert response.status_code == 200 + + sse_events = [ + json.loads(line.removeprefix("data: ")) + for line in response.text.splitlines() + if line.startswith("data: ") + ] + + assert len(sse_events) == 1 + assert sse_events[0]["content"]["parts"][0]["text"] == "LLM reply" + assert sse_events[0]["actions"]["artifactDelta"] == {"artifact.txt": 0} + + def test_agent_run_sse_yields_error_object_on_exception( test_app, create_test_session, monkeypatch ):