♻️ Reproduction Steps
- Create a LangGraph agent with custom state fields (e.g.,
thinking_steps, run_titles, current_run_id)
- Use
copilotkit_emit_state to emit partial state updates during node execution:
# In node, emit only specific fields
await copilotkit_emit_state(config, {"current_run_id": run_id, "current_turn_index": turn_index})
# Later, emit different fields
await copilotkit_emit_state(config, {"thinking_steps": new_steps})
# Even later, emit another field
await copilotkit_emit_state(config, {"run_titles": current_titles})
- Observe frontend
useAgent hook - each STATE_SNAPSHOT event replaces previous state instead of merging
🔎 Observed Behavior
When copilotkit_emit_state emits partial state (e.g., {"run_titles": {...}}):
- The
STATE_SNAPSHOT event contains only {"run_titles": {...}}
- Frontend loses previously emitted fields like
thinking_steps
- State "flickers" - fields appear, disappear, then reappear
Console logs showing the issue:
[useAgent] state: {current_run_id: '...', current_turn_index: 6}
[useAgent] state: {thinking_steps: Array(43)} // ✅ has steps
[useAgent] state: {run_titles: {...}} // ❌ thinking_steps GONE!
[useAgent] state: {thinking_steps: Array(43)} // ✅ back again
✅ Expected Behavior
According to documentation:
"the underlying mechanism in _emit_state_sync_event will merge this with the current_graph_state before emitting a complete state object to the frontend"
Each STATE_SNAPSHOT should contain the merged state, preserving all fields.
📃 Root Cause Analysis
In langgraph_agent.py (v0.1.76 and v0.1.77), lines 437-447:
if manually_emit_intermediate_state:
manually_emitted_state = cast(Any, event["data"])
yield self._emit_state_sync_event(
thread_id=thread_id,
run_id=run_id,
node_name=node_name,
state=manually_emitted_state, # ❌ Only partial state!
running=True,
active=True
) + "\n"
continue # ❌ Skips merge with current_graph_state
The continue statement bypasses lines 479-482 where normal state sync merges with current_graph_state.
💡 Suggested Fix
Merge the manually emitted state with current_graph_state before emitting:
if manually_emit_intermediate_state:
manually_emitted_state = cast(Any, event["data"])
# Merge with current graph state
merged_state = {**current_graph_state, **manually_emitted_state}
current_graph_state.update(manually_emitted_state)
yield self._emit_state_sync_event(
thread_id=thread_id,
run_id=run_id,
node_name=node_name,
state=merged_state, # ✅ Full merged state
running=True,
active=True
) + "\n"
continue
📦 Versions
- copilotkit (Python SDK): 0.1.76 / 0.1.77
- @copilotkit/react-core: 1.51.2
- langgraph: 1.0.7+
🔗 Related Issues
♻️ Reproduction Steps
thinking_steps,run_titles,current_run_id)copilotkit_emit_stateto emit partial state updates during node execution:useAgenthook - eachSTATE_SNAPSHOTevent replaces previous state instead of merging🔎 Observed Behavior
When
copilotkit_emit_stateemits partial state (e.g.,{"run_titles": {...}}):STATE_SNAPSHOTevent contains only{"run_titles": {...}}thinking_stepsConsole logs showing the issue:
✅ Expected Behavior
According to documentation:
Each
STATE_SNAPSHOTshould contain the merged state, preserving all fields.📃 Root Cause Analysis
In
langgraph_agent.py(v0.1.76 and v0.1.77), lines 437-447:The
continuestatement bypasses lines 479-482 where normal state sync merges withcurrent_graph_state.💡 Suggested Fix
Merge the manually emitted state with
current_graph_statebefore emitting:📦 Versions
🔗 Related Issues