Skip to content

fix: treat GraphInterrupt as default-level in @observe decorator#1687

Open
michaelxer wants to merge 1 commit into
langfuse:mainfrom
michaelxer:fix-graph-interrupt-error-level
Open

fix: treat GraphInterrupt as default-level in @observe decorator#1687
michaelxer wants to merge 1 commit into
langfuse:mainfrom
michaelxer:fix-graph-interrupt-error-level

Conversation

@michaelxer
Copy link
Copy Markdown

@michaelxer michaelxer commented Jun 4, 2026

Problem

LangGraph HITL interrupts (GraphInterrupt, NodeInterrupt) are incorrectly logged as level="ERROR" when using the @observe decorator. The CallbackHandler already handles this correctly via CONTROL_FLOW_EXCEPTION_TYPES, but @observe does not check for control flow exceptions.

Root Cause

In observe.py, all 4 exception handlers unconditionally set level="ERROR":

  • Async wrapper (line ~303)
  • Sync wrapper (line ~394)
  • Sync generator wrapper (line ~592)
  • Async generator wrapper (line ~687)

Fix

  1. New shared utility langfuse/_utils/control_flow.py: exports get_error_level(error) that returns "DEFAULT" for GraphBubbleUp subclasses, "ERROR" for everything else

  2. Modified observe.py: all 4 handlers now use level=get_error_level(e) instead of hardcoded "ERROR"

  3. Modified CallbackHandler.py: imports from shared utility instead of maintaining its own copy

Verification

  • GraphInterruptlevel="DEFAULT"
  • NodeInterruptlevel="DEFAULT"
  • RuntimeErrorlevel="ERROR"
  • Both code paths use the same shared logic ✓

Fixes langfuse/langfuse#14034

Greptile Summary

This PR fixes GraphInterrupt and NodeInterrupt (LangGraph HITL control-flow exceptions) being incorrectly logged at level="ERROR" by the @observe decorator. It introduces a shared get_error_level() utility in langfuse/_utils/control_flow.py and applies it to all four exception-handling paths in observe.py, while also refactoring CallbackHandler.py to use the same utility instead of its own inline copy.

  • New langfuse/_utils/control_flow.py: Exports get_error_level(error) and CONTROL_FLOW_EXCEPTION_TYPES; safely optional-imports GraphBubbleUp from langgraph.
  • observe.py: All four handlers (async, sync, sync-generator, async-generator) now call get_error_level(e) instead of hardcoding "ERROR".
  • CallbackHandler.py: _get_error_level_and_status_message delegates to get_error_level; CONTROL_FLOW_EXCEPTION_TYPES is imported but no longer referenced.

Confidence Score: 4/5

Safe to merge; the logic change is correct and well-scoped, with only minor housekeeping issues in CallbackHandler.py.

The core fix is straightforward and correct — get_error_level is properly shared across both code paths. The only issues are in CallbackHandler.py: an import placed after module-level constants instead of at the top of the file, and CONTROL_FLOW_EXCEPTION_TYPES being imported but never actually used after the refactor.

langfuse/langchain/CallbackHandler.py — the import placement and unused symbol should be cleaned up.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Exception raised in decorated function] --> B{get_error_level}
    B --> C{Is instance of\nCONTROL_FLOW_EXCEPTION_TYPES?}
    C -- Yes\n e.g. GraphInterrupt\nNodeInterrupt --> D[level = DEFAULT]
    C -- No\n e.g. RuntimeError\nValueError --> E[level = ERROR]
    D --> F[span.update\nlevel=DEFAULT, status_message]
    E --> G[span.update\nlevel=ERROR, status_message]
    F --> H[Re-raise exception]
    G --> H

    subgraph control_flow.py
        B
        C
    end

    subgraph observe.py
        F
        G
    end

    subgraph CallbackHandler.py
        I[_get_error_level_and_status_message] --> B
    end
Loading
Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 2
langfuse/langchain/CallbackHandler.py:90
`CONTROL_FLOW_EXCEPTION_TYPES` is imported here but is never referenced anywhere else in `CallbackHandler.py` — the refactored `_get_error_level_and_status_message` now delegates entirely to `get_error_level()`, so only `get_error_level` is needed. The unused symbol will trigger linting/import-checker warnings.

```suggestion
from langfuse._utils.control_flow import get_error_level
```

### Issue 2 of 2
langfuse/langchain/CallbackHandler.py:90
**Import placed after module-level constants**
This import lands at line 90, after the constant definitions on lines 87–88, which breaks the convention of keeping all imports at the top of the module. It should be moved up alongside the other `langfuse._utils` and `langfuse._client` imports in the preamble.

Reviews (1): Last reviewed commit: "fix: treat GraphInterrupt as default-lev..." | Re-trigger Greptile

Context used:

  • Rule used - Move imports to the top of the module instead of p... (source)

Learned From
langfuse/langfuse-python#1387

…d CallbackHandler

LangGraph HITL interrupts (GraphInterrupt, NodeInterrupt) were logged as
level=ERROR through the @observe decorator path. The CallbackHandler already
handled this via CONTROL_FLOW_EXCEPTION_TYPES, but @observe did not.

Extract shared control flow exception utility so both paths use the same
logic. GraphBubbleUp subclasses now return level=DEFAULT consistently.

Fixes #14034
Copy link
Copy Markdown

@claude claude Bot left a comment

Choose a reason for hiding this comment

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

Claude Code Review

This pull request is from a fork — automated review is disabled. A repository maintainer can comment @claude review to run a one-time review.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

GraphInterrupt (LangGraph HITL interrupt) incorrectly marked as ERROR in Langfuse traces via OTEL

1 participant