Skip to content

Commit 08fbb79

Browse files
jpr5AlemTuzlak
authored andcommitted
fix(sdk-python): copilotkit_emit_state merges state instead of replacing (CopilotKit#3138)
Sequential emit_state calls with different keys now preserve all keys in the snapshot. Previously, each call would replace the entire manually_emitted_state, losing keys from earlier calls.
1 parent f4ccc21 commit 08fbb79

2 files changed

Lines changed: 60 additions & 1 deletion

File tree

sdk-python/copilotkit/langgraph_agent.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -435,7 +435,7 @@ async def _stream_events( # pylint: disable=too-many-locals
435435
manually_emitted_state = None
436436

437437
if manually_emit_intermediate_state:
438-
manually_emitted_state = cast(Any, event["data"])
438+
manually_emitted_state = _merge_emit_state(current_graph_state, cast(Any, event["data"]))
439439
yield self._emit_state_sync_event(
440440
thread_id=thread_id,
441441
run_id=run_id,
@@ -724,6 +724,13 @@ async def get_checkpoint_before_message(self, message_id: str, thread_id: str):
724724

725725
raise ValueError("Message ID not found in history")
726726

727+
728+
def _merge_emit_state(current_state: dict, emitted_state: Any) -> dict:
729+
"""Merge emitted state on top of current graph state instead of replacing it."""
730+
if isinstance(emitted_state, dict):
731+
return {**current_state, **emitted_state}
732+
return cast(Any, emitted_state)
733+
727734
class _StreamingStateExtractor:
728735
def __init__(self, emit_intermediate_state: List[dict]):
729736
self.emit_intermediate_state = emit_intermediate_state
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
"""Tests for #3138: copilotkit_emit_state should merge state, not replace it."""
2+
3+
import pytest
4+
from unittest.mock import MagicMock, patch, AsyncMock
5+
from typing import Any, cast
6+
7+
8+
class TestEmitStateMerge:
9+
"""Verify that sequential emit_state calls merge keys rather than overwriting."""
10+
11+
def test_sequential_emit_state_preserves_both_keys(self):
12+
"""Two emit_state calls with different keys should both be present in the snapshot.
13+
14+
Bug: manually_emitted_state = cast(Any, event["data"]) replaces entire state.
15+
Fix: manually_emitted_state = {**current_graph_state, **cast(Any, event["data"])}
16+
"""
17+
# Simulate the state tracking logic from langgraph_agent.py
18+
# This mirrors lines ~410-460 of langgraph_agent.py
19+
current_graph_state = {"existing_key": "existing_value"}
20+
manually_emitted_state = None
21+
22+
# First emit_state call: emit {"progress": 50}
23+
event1_data = {"progress": 50}
24+
25+
# BUG LINE (before fix): manually_emitted_state = cast(Any, event1_data)
26+
# FIX LINE: manually_emitted_state = {**current_graph_state, **cast(Any, event1_data)}
27+
from copilotkit.langgraph_agent import _merge_emit_state
28+
manually_emitted_state = _merge_emit_state(current_graph_state, event1_data)
29+
30+
# After first emit, should have both existing_key and progress
31+
assert "existing_key" in manually_emitted_state
32+
assert manually_emitted_state["progress"] == 50
33+
34+
# Second emit_state call: emit {"status": "running"}
35+
event2_data = {"status": "running"}
36+
# The updated_state line uses: manually_emitted_state or current_graph_state
37+
# So manually_emitted_state is what gets sent as the snapshot
38+
updated_state = manually_emitted_state or current_graph_state
39+
# Now merge the second emit on top
40+
manually_emitted_state = _merge_emit_state(updated_state, event2_data)
41+
42+
# Both keys from both emits AND the original state must be present
43+
assert manually_emitted_state["existing_key"] == "existing_value"
44+
assert manually_emitted_state["progress"] == 50
45+
assert manually_emitted_state["status"] == "running"
46+
47+
def test_emit_state_overwrites_same_key(self):
48+
"""Emitting the same key twice should use the latest value."""
49+
from copilotkit.langgraph_agent import _merge_emit_state
50+
current_graph_state = {"progress": 0}
51+
result = _merge_emit_state(current_graph_state, {"progress": 100})
52+
assert result["progress"] == 100

0 commit comments

Comments
 (0)