Skip to content

feat(callbacks): add MermaidTrace handler for sequence diagram visualization#519

Open
xt765 wants to merge 7 commits intolangchain-ai:mainfrom
xt765:feat/mermaid-trace
Open

feat(callbacks): add MermaidTrace handler for sequence diagram visualization#519
xt765 wants to merge 7 commits intolangchain-ai:mainfrom
xt765:feat/mermaid-trace

Conversation

@xt765
Copy link

@xt765 xt765 commented Feb 3, 2026

Description

Disclaimer: This contribution was developed with the assistance of an AI programming agent (Trae/Gemini).

This PR introduces the MermaidTraceCallbackHandler in langchain-community, allowing users to visualize LangChain execution flows as Mermaid sequence diagrams.

Why this solution is right:

  • Zero-Invasive: Implemented as a standard callback, requiring no changes to existing chain logic.
  • Production-Ready Thread Safety: Uses a robust _run_to_root mapping and threading.Lock to correctly isolate concurrent requests.
  • Lazy Integration: Uses guard_import for the mermaid-trace dependency, ensuring it doesn't affect users who don't use this feature.

Key Fixes & Improvements:

  • Graphite AI Compliance: Resolved all Critical issues identified by the Graphite Agent, including:
    • Use-after-delete Bug: Refactored _end_run to safely extract parent metadata before cleaning up internal mappings, preventing potential crashes or data corruption in end/error events.
    • Race Condition Protection: Fixed a concurrency risk by ensuring that the participant stack is copied during retrieval, preventing simultaneous read/write conflicts.
  • Event Logic Correction: Fixed a logic error where source/target participants were reversed in end-of-run events.

Areas for careful review:

  • The logic for tracking participant stacks via root_run_id in mermaid_trace.py.
  • Lifecycle pairing to ensure internal mappings are cleaned up after execution.

Issue

N/A

Dependencies

This feature requires the optional mermaid-trace package for diagram generation. It is handled via lazy import.

Testing

All tests passed locally using pytest, ruff, and mypy.

  • Unit Tests: Full coverage of Chain, LLM, Tool, Agent, and Retriever lifecycles in libs/community/tests/unit_tests/callbacks/test_mermaid_trace.py.
  • Concurrency Test: Verified stack isolation and thread safety under high load in libs/community/tests/unit_tests/callbacks/test_mermaid_trace_concurrency.py.
  • Integration Tests: Verified real-world LangChain execution flows in libs/community/tests/integration_tests/callbacks/test_mermaid_trace.py.
  • Quality: Passed ruff check, ruff format, and mypy --disallow-untyped-defs.

xt765 added 4 commits February 3, 2026 10:59
- Implement MermaidTraceCallbackHandler in callbacks/mermaid_trace.py
- Register handler in callbacks/__init__.py
- Add unit tests in tests/unit_tests/callbacks/test_mermaid_trace.py
- Update extended_testing_deps.txt with mermaid-trace dependency
- Ensure compatibility with langchain-core BaseCallbackHandler signatures
- Fix linting and type checking issues
…TraceCallbackHandler

- Implement thread-safe participant stack using threading.local()
- Cache mermaid-trace module to avoid repetitive imports
- Refactor methods to use centralized trace ID and source retrieval
- Optimize performance by reducing redundant function calls
chain.invoke({"input": "hello"}, config={"callbacks": [handler]})

# Verify stack is empty after error
assert len(handler._participant_stack) == 0
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Look for Suspicious or Risky Code: The code references handler._participant_stack which does not exist in the MermaidTraceCallbackHandler class. The actual implementation uses _root_to_stack dictionary to track participant stacks per root run. This will cause an AttributeError at runtime. The test should either use the public API or reference the correct internal attribute like handler._root_to_stack.

Suggested change
assert len(handler._participant_stack) == 0
assert all(len(stack) == 0 for stack in handler._root_to_stack.values())

Spotted by Graphite Agent (based on custom rule: Code quality)

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

Comment on lines 210 to 214
target = self._end_run(run_id)
if not target:
return

source = self._get_current_source(run_id)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Critical Bug: Use-after-delete causes incorrect source participant

The _end_run() method deletes _run_to_root[run_id] on line 112, but then _get_current_source(run_id) is called which needs this mapping to find the participant stack. This causes _get_participant_stack() to return an empty list, making _get_current_source() fall back to the LogContext default instead of using the correct parent from the stack.

Impact: Return/error messages in the sequence diagram will have incorrect source participants, breaking the visualization.

Fix: Call _get_current_source(run_id) BEFORE _end_run(run_id):

def on_chain_end(self, outputs: Dict[str, Any], *, run_id: uuid.UUID, ...) -> None:
    source = self._get_current_source(run_id)  # Move before _end_run
    target = self._end_run(run_id)
    if not target:
        return
    # rest of method...

This affects all on_*_end and on_*_error methods (lines 194-260, 346-412, 458-524, 568-634).

Suggested change
target = self._end_run(run_id)
if not target:
return
source = self._get_current_source(run_id)
source = self._get_current_source(run_id)
target = self._end_run(run_id)
if not target:
return

Spotted by Graphite Agent

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

Comment on lines 55 to 68
def _get_participant_stack(self, run_id: uuid.UUID) -> List[str]:
"""Get the participant stack for the given run_id.

Args:
run_id: The run ID.

Returns:
The participant stack.
"""
with self._lock:
root_id = self._run_to_root.get(run_id)
if root_id is None:
return []
return self._root_to_stack.get(root_id, [])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Critical Race Condition: Returns mutable list reference without lock protection

The method returns a direct reference to the internal stack list, which can be modified by other threads after the lock is released. This causes race conditions when callers use the returned stack.

Impact: In _get_current_source (line 144-146), after getting the stack reference and releasing the lock, another thread could call _end_run and pop from the stack, leading to:

  • IndexError if stack becomes empty between the if stack: check and stack[-1] access
  • Wrong participant name if a different element is now at the top

Fix:

def _get_participant_stack(self, run_id: uuid.UUID) -> List[str]:
    with self._lock:
        root_id = self._run_to_root.get(run_id)
        if root_id is None:
            return []
        return list(self._root_to_stack.get(root_id, []))  # Return a copy
Suggested change
def _get_participant_stack(self, run_id: uuid.UUID) -> List[str]:
"""Get the participant stack for the given run_id.
Args:
run_id: The run ID.
Returns:
The participant stack.
"""
with self._lock:
root_id = self._run_to_root.get(run_id)
if root_id is None:
return []
return self._root_to_stack.get(root_id, [])
def _get_participant_stack(self, run_id: uuid.UUID) -> List[str]:
"""Get the participant stack for the given run_id.
Args:
run_id: The run ID.
Returns:
The participant stack.
"""
with self._lock:
root_id = self._run_to_root.get(run_id)
if root_id is None:
return []
return list(self._root_to_stack.get(root_id, [])) # Return a copy

Spotted by Graphite Agent

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

@github-actions github-actions bot added feature and removed feature labels Feb 3, 2026
@xt765
Copy link
Author

xt765 commented Feb 3, 2026

Updated the PR with fixes for all critical issues identified by Graphite Agent. Local tests and linting are all passing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant