-
Notifications
You must be signed in to change notification settings - Fork 3.8k
Description
Checked other resources
- This is a bug, not a usage question. For questions, please use the LangChain Forum (https://forum.langchain.com/).
- I added a clear and detailed title that summarizes the issue.
- I read what a minimal reproducible example is (https://stackoverflow.com/help/minimal-reproducible-example).
- I included a self-contained, minimal example that demonstrates the issue INCLUDING all the relevant imports. The code run AS IS to reproduce the issue.
Example Code
"""
Minimal Reproducible Example: LangSmith cannot serialize dict with tuple keys
Run: LANGCHAIN_TRACING_V2=true python minimal_repro.py
Expected: Both graphs execute successfully, but ONLY the problematic pattern
shows "Error in LangChainTracer.on_chain_end callback: TypeError(...tuple)"
"""
import asyncio
import operator
from typing import TypedDict, Annotated
from langgraph.graph import StateGraph, START, END
from langgraph.types import Send
# ============================================================================
# PROBLEMATIC: Dict with tuple keys - breaks LangSmith trace serialization
# ============================================================================
class BadState(TypedDict):
# operator.or_ used for combining dictionaries, equivalent to `dict1 | dict2`
results: Annotated[dict[tuple[str, str], str], operator.or_]
def bad_fanout(state: BadState):
return [Send("bad_process", {"id": str(i)}) for i in range(2)]
async def bad_process(state: BadState) -> dict:
await asyncio.sleep(0.01)
# Returns dict with tuple key - BREAKS SERIALIZATION
return {"results": {("key", state["id"]): f"value_{state['id']}"}}
def build_bad_graph():
graph = StateGraph(BadState)
graph.add_node("bad_process", bad_process)
graph.add_conditional_edges(START, bad_fanout, ["bad_process"])
graph.add_edge("bad_process", END)
return graph.compile()
# ============================================================================
# WORKING: List with tuples - serializes fine
# ============================================================================
class GoodState(TypedDict):
results: Annotated[list[tuple[str, str, str]], operator.add]
def good_fanout(state: GoodState):
return [Send("good_process", {"id": str(i)}) for i in range(2)]
async def good_process(state: GoodState) -> dict:
await asyncio.sleep(0.01)
# Returns list with tuple - WORKS FINE
return {"results": [("key", state["id"], f"value_{state['id']}")]}
def build_good_graph():
graph = StateGraph(GoodState)
graph.add_node("good_process", good_process)
graph.add_conditional_edges(START, good_fanout, ["good_process"])
graph.add_edge("good_process", END)
return graph.compile()
# ============================================================================
# TEST
# ============================================================================
async def main():
print("\n" + "="*60)
print("Testing PROBLEMATIC pattern (dict with tuple keys)...")
print("="*60)
bad_graph = build_bad_graph()
result = await bad_graph.ainvoke({}, config={"run_name": "bad_pattern"})
print(f"✓ Execution succeeded: {len(result['results'])} results")
print("⚠️ But check logs above for serialization error ^^^")
await asyncio.sleep(0.5)
print("\n" + "="*60)
print("Testing WORKING pattern (list with tuples)...")
print("="*60)
good_graph = build_good_graph()
result = await good_graph.ainvoke({}, config={"run_name": "good_pattern"})
print(f"✓ Execution succeeded: {len(result['results'])} results")
print("✓ No serialization errors")
print("\n" + "="*60)
print("ISSUE: Dict with tuple keys breaks LangSmith serialization")
print(" Error: 'keys must be str, int, float, bool or None'")
print("FIX: Use list[tuple] with operator.add instead")
print("="*60 + "\n")
if __name__ == "__main__":
asyncio.run(main())Error Message and Stack Trace (if applicable)
$ LANGCHAIN_TRACING_V2=true python minimal_repro.py
============================================================
Testing PROBLEMATIC pattern (dict with tuple keys)...
============================================================
keys must be str, int, float, bool or None, not tuple
Error in LangChainTracer.on_chain_end callback: TypeError('keys must be str, int, float, bool or None, not tuple')
Error in LangChainTracer.on_chain_end callback: TypeError('keys must be str, int, float, bool or None, not tuple')
Error in LangChainTracer.on_chain_end callback: TypeError('keys must be str, int, float, bool or None, not tuple')
✓ Execution succeeded: 2 results
⚠️ But check logs above for serialization error ^^^
============================================================
Testing WORKING pattern (list with tuples)...
============================================================
✓ Execution succeeded: 2 results
✓ No serialization errors
============================================================
ISSUE: Dict with tuple keys breaks LangSmith serialization
Error: 'keys must be str, int, float, bool or None'
FIX: Use list[tuple] with operator.add instead
============================================================Description
LangSmith trace never completes when LangGraph state reduces dict with tuple keys
Problem
When using LangGraph with LangSmith tracing enabled, traces never show completion and latency remains unavailable if the state TypedDict contains a dictionary with tuple keys, even though graph execution completes successfully.
What I'm doing
I'm using LangGraph's parallel execution pattern with Send for fan-out, and accumulating results in state using operator.or_ with a dict that has tuple keys:
class State(TypedDict):
results_map: Annotated[dict[tuple[str, str], str], operator.or_]
async def process_node(state: State) -> dict:
return {"results_map": {(key1, key2): value}}I run the script with the tracing environmental variable set to true:
LANGCHAIN_TRACING_V2=true python minimal_repro.pyExpected behavior
- Graph executes successfully ✓
- Returns correct results ✓
- LangSmith trace shows completion with latency metrics ✗
- No errors in logs ✗
Actual behavior
- Graph executes successfully ✓
- Returns correct results ✓
- Error in logs:
Error in LangChainTracer.on_chain_end callback: TypeError('keys must be str, int, float, bool or None, not tuple') - LangSmith trace never completes - shows as "running" indefinitely
- Latency metrics unavailable in dashboard
Hypothesis
LangSmith's tracer serializes state to JSON for observability. Python's JSON encoder only supports primitive types (str, int, float, bool, None) as dictionary keys. When state contains dict[tuple, Any], the serialization callback fails silently, preventing trace completion.
Workaround
Use operator.add with list of tuples instead of operator.or_ with dict:
class State(TypedDict):
results_list: Annotated[list[tuple[str, str, str]], operator.add]
async def process_node(state: State) -> dict:
return {"results_list": [(key1, key2, value)]}
# Convert to dict in aggregation node if needed
results_map = {(k1, k2): v for k1, k2, v in state["results_list"]}This successfully allows traces to complete with full metrics.
Minimal reproduction
See attached minimal_repro.py - demonstrates both problematic and working patterns in ~100 lines.
System Info
System Information
OS: Linux
OS Version: #1 SMP PREEMPT_DYNAMIC Thu Jun 5 18:30:46 UTC 2025
Python Version: 3.11.13 (main, Jul 23 2025, 00:34:47) [Clang 20.1.4 ]
Package Information
langchain_core: 1.0.4
langsmith: 0.4.42
langgraph_sdk: 0.2.9
Optional packages not installed
langserve
Other Dependencies
httpx: 0.28.1
jsonpatch: 1.33
orjson: 3.11.4
packaging: 25.0
pydantic: 2.12.4
pyyaml: 6.0.3
requests: 2.32.5
requests-toolbelt: 1.0.0
tenacity: 9.1.2
typing-extensions: 4.15.0
zstandard: 0.25.0