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 <gweale@google.com>
PiperOrigin-RevId: 877435561
This commit is contained in:
George Weale
2026-03-02 09:25:00 -08:00
committed by Copybara-Service
parent 991abd44e9
commit 6a929af718
2 changed files with 55 additions and 1 deletions
+4 -1
View File
@@ -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
):
+51
View File
@@ -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
):