From 7ef5c546c351d54e4263e3c803a0e418f55ce082 Mon Sep 17 00:00:00 2001 From: "L. Elaine Dazzio" Date: Fri, 27 Mar 2026 21:24:35 -0400 Subject: [PATCH 1/2] fix: guard EventEncoder against non-BaseEvent objects (#4929) EventEncoder.encode() calls model_dump_json() internally, which fails with AttributeError when non-Pydantic objects (like AgentResponseUpdate) leak through the AG-UI event pipeline. This adds a defensive isinstance(event, BaseEvent) check in the SSE event_generator() so that non-BaseEvent objects are skipped with a warning instead of crashing the stream. Fixes #4929 --- .../ag-ui/agent_framework_ag_ui/_endpoint.py | 15 +++++++- .../ag-ui/tests/ag_ui/test_endpoint.py | 34 +++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/python/packages/ag-ui/agent_framework_ag_ui/_endpoint.py b/python/packages/ag-ui/agent_framework_ag_ui/_endpoint.py index d80ecea7a1..f4116b221e 100644 --- a/python/packages/ag-ui/agent_framework_ag_ui/_endpoint.py +++ b/python/packages/ag-ui/agent_framework_ag_ui/_endpoint.py @@ -9,7 +9,7 @@ from collections.abc import AsyncGenerator, Sequence from typing import Any -from ag_ui.core import RunErrorEvent +from ag_ui.core import BaseEvent, RunErrorEvent from ag_ui.encoder import EventEncoder from agent_framework import SupportsAgentRun, Workflow from fastapi import FastAPI, HTTPException @@ -93,6 +93,19 @@ async def event_generator() -> AsyncGenerator[str]: event_count = 0 try: async for event in protocol_runner.run(input_data): + # Guard: only BaseEvent instances can be SSE-encoded. + # Non-BaseEvent objects (e.g. AgentResponseUpdate) lack + # model_dump_json() and would cause an AttributeError + # in EventEncoder.encode(). Skip them with a warning. + if not isinstance(event, BaseEvent): + logger.warning( + "[%s] Skipping non-BaseEvent object of type %s; " + "only ag_ui.core.BaseEvent instances can be SSE-encoded.", + path, + type(event).__name__, + ) + continue + event_count += 1 event_type_name = getattr(event, "type", type(event).__name__) # Log important events at INFO level diff --git a/python/packages/ag-ui/tests/ag_ui/test_endpoint.py b/python/packages/ag-ui/tests/ag_ui/test_endpoint.py index 51ab468b84..39d2ad15b1 100644 --- a/python/packages/ag-ui/tests/ag_ui/test_endpoint.py +++ b/python/packages/ag-ui/tests/ag_ui/test_endpoint.py @@ -9,6 +9,7 @@ from ag_ui.core import RunStartedEvent from agent_framework import ( Agent, + AgentResponseUpdate, ChatResponseUpdate, Content, WorkflowBuilder, @@ -603,3 +604,36 @@ async def run(self, input_data: dict[str, Any]): # Should still get 200 (SSE stream), just with no events assert response.status_code == 200 + + +async def test_endpoint_skips_non_base_event_objects(): + """Non-BaseEvent objects (e.g. AgentResponseUpdate) are skipped gracefully. + + Regression test for https://github.com/microsoft/agent-framework/issues/4929 + """ + + class MixedEventWorkflow(AgentFrameworkWorkflow): + async def run(self, input_data: dict[str, Any]): + del input_data + yield RunStartedEvent(run_id="run-1", thread_id="thread-1") + # Yield a non-BaseEvent object — this should be skipped, not crash + yield AgentResponseUpdate( # type: ignore[misc] + contents=[Content.from_text(text="leaked update")], + role="assistant", + ) + + app = FastAPI() + add_agent_framework_fastapi_endpoint(app, MixedEventWorkflow(), path="/mixed-events") + client = TestClient(app) + + response = client.post("/mixed-events", json={"messages": [{"role": "user", "content": "Hello"}]}) + + assert response.status_code == 200 + content = response.content.decode("utf-8") + lines = [line for line in content.split("\n") if line.startswith("data: ")] + event_types = [json.loads(line[6:]).get("type") for line in lines] + + # RUN_STARTED should be present; the AgentResponseUpdate should have been + # silently skipped — no RUN_ERROR or crash. + assert "RUN_STARTED" in event_types + assert "RUN_ERROR" not in event_types From e79c175149394e9bafea21395175e86d1057638b Mon Sep 17 00:00:00 2001 From: "L. Elaine Dazzio" Date: Fri, 27 Mar 2026 21:54:32 -0400 Subject: [PATCH 2/2] fix: update test wording from "silently skipped" to "skipped with a warning" MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address Copilot review comment — the endpoint implementation logs a warning when skipping non-BaseEvent objects, so the test docstring and inline comments should reflect that behavior accurately. --- python/packages/ag-ui/tests/ag_ui/test_endpoint.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/python/packages/ag-ui/tests/ag_ui/test_endpoint.py b/python/packages/ag-ui/tests/ag_ui/test_endpoint.py index 39d2ad15b1..80e7f1abaf 100644 --- a/python/packages/ag-ui/tests/ag_ui/test_endpoint.py +++ b/python/packages/ag-ui/tests/ag_ui/test_endpoint.py @@ -607,7 +607,7 @@ async def run(self, input_data: dict[str, Any]): async def test_endpoint_skips_non_base_event_objects(): - """Non-BaseEvent objects (e.g. AgentResponseUpdate) are skipped gracefully. + """Non-BaseEvent objects (e.g. AgentResponseUpdate) are skipped with a warning. Regression test for https://github.com/microsoft/agent-framework/issues/4929 """ @@ -616,7 +616,7 @@ class MixedEventWorkflow(AgentFrameworkWorkflow): async def run(self, input_data: dict[str, Any]): del input_data yield RunStartedEvent(run_id="run-1", thread_id="thread-1") - # Yield a non-BaseEvent object — this should be skipped, not crash + # Yield a non-BaseEvent object — this should be skipped with a warning, not crash yield AgentResponseUpdate( # type: ignore[misc] contents=[Content.from_text(text="leaked update")], role="assistant", @@ -634,6 +634,6 @@ async def run(self, input_data: dict[str, Any]): event_types = [json.loads(line[6:]).get("type") for line in lines] # RUN_STARTED should be present; the AgentResponseUpdate should have been - # silently skipped — no RUN_ERROR or crash. + # skipped with a warning — no RUN_ERROR or crash. assert "RUN_STARTED" in event_types assert "RUN_ERROR" not in event_types