Skip to content

Map-reduce fails to trace when reducing dictionary with tuple keys #6435

@Jichao-Yang

Description

@Jichao-Yang

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.py

Expected 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingpendingawaiting review/confirmation by maintainer

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions