From a7ed7a18e924deffc20fca99efe23dbb00ad6c9f Mon Sep 17 00:00:00 2001 From: Brian Love Date: Sat, 16 May 2026 08:35:16 -0700 Subject: [PATCH] fix(c-generative-ui): switch emit_state to get_stream_writer (LangGraph 1.x) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The dashboard's KPI cards / charts / tables never populated past the "Building UI…" skeleton. Root cause: `adispatch_custom_event` no longer flows into the `custom` stream channel in LangGraph 1.x. Live verification via SSE shows 0 custom events with adispatch and 1 custom event with get_stream_writer(). Additional fix: the payload now matches the chat-lib bridge's expected shape — `{name: "state_update", data: }`. The bridge (stream-manager.bridge.ts) reads `eventData.name` for routing and `eventData.data` for the flat path→value patches that signal-state-store.update() needs. Verified end-to-end via chrome MCP — all 7 state paths now populate correctly (4 stat_cards, 1 line_chart, 1 bar_chart, 1 data_grid) with zero render-default-fallback components remaining and zero NG0303 input-spam errors. Co-Authored-By: Claude Opus 4.7 --- cockpit/chat/generative-ui/python/src/graph.py | 16 +++++++++++++--- .../streaming/python/src/dashboard_graph.py | 16 +++++++++++++--- 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/cockpit/chat/generative-ui/python/src/graph.py b/cockpit/chat/generative-ui/python/src/graph.py index 815e5f369..af02264d6 100644 --- a/cockpit/chat/generative-ui/python/src/graph.py +++ b/cockpit/chat/generative-ui/python/src/graph.py @@ -67,8 +67,13 @@ def should_call_tools(state: DashboardState) -> Literal["call_tools", "respond"] async def emit_state(state: DashboardState) -> DashboardState: - """Emit state_update custom events from tool results.""" - from langchain_core.callbacks import adispatch_custom_event + """Emit state_update custom events from tool results. + + Uses LangGraph 1.x's `get_stream_writer()` — `adispatch_custom_event` + no longer flows into the `custom` stream channel. The chat-lib bridge + parses the payload as `{name: 'state_update', data: }`. + """ + from langgraph.config import get_stream_writer tool_results = {} for msg in reversed(state["messages"]): @@ -93,7 +98,12 @@ async def emit_state(state: DashboardState) -> DashboardState: break if tool_results: - await adispatch_custom_event("state_update", {"updates": tool_results}) + # The chat-lib bridge (stream-manager.bridge.ts handles the 'custom' + # case) parses `eventData['name']` for routing and `eventData['data']` + # for payload; chat.component.ts then forwards data to + # signal-state-store.update() which expects flat Record. + writer = get_stream_writer() + writer({"name": "state_update", "data": tool_results}) return state diff --git a/cockpit/langgraph/streaming/python/src/dashboard_graph.py b/cockpit/langgraph/streaming/python/src/dashboard_graph.py index 7ce329aa6..d9e849f54 100644 --- a/cockpit/langgraph/streaming/python/src/dashboard_graph.py +++ b/cockpit/langgraph/streaming/python/src/dashboard_graph.py @@ -67,8 +67,13 @@ def should_call_tools(state: DashboardState) -> Literal["call_tools", "respond"] async def emit_state(state: DashboardState) -> DashboardState: - """Emit state_update custom events from tool results.""" - from langchain_core.callbacks import adispatch_custom_event + """Emit state_update custom events from tool results. + + Uses LangGraph 1.x's `get_stream_writer()` — `adispatch_custom_event` + no longer flows into the `custom` stream channel. The chat-lib bridge + parses the payload as `{name: 'state_update', data: }`. + """ + from langgraph.config import get_stream_writer tool_results = {} for msg in reversed(state["messages"]): @@ -94,7 +99,12 @@ async def emit_state(state: DashboardState) -> DashboardState: break if tool_results: - await adispatch_custom_event("state_update", {"updates": tool_results}) + # The chat-lib bridge (stream-manager.bridge.ts handles the 'custom' + # case) parses `eventData['name']` for routing and `eventData['data']` + # for payload; chat.component.ts then forwards data to + # signal-state-store.update() which expects flat Record. + writer = get_stream_writer() + writer({"name": "state_update", "data": tool_results}) return state