Picture this: It's 2 AM, and your production LangGraph agent is failing silently. You see ConnectionError: timeout in your logs, but the error message gives you nothing. The graph execution halts at Node B, but you're not sure if the issue is in Node A's output or the state transition logic. Sound familiar? I've been there—wrestling with invisible state mutations and opaque graph execution flows that make debugging feel like reading tea leaves.
In this comprehensive guide, I'll walk you through battle-tested debugging strategies for LangGraph applications, complete with real error scenarios, step-by-step troubleshooting workflows, and production-ready code you can copy-paste today. By the end, you'll have a complete toolkit for turning cryptic failures into actionable insights.
Why LangGraph Debugging Requires a Different Approach
Unlike traditional Python applications where you can drop a print() statement and call it a day, LangGraph operates on a fundamentally different execution model. Your application is a directed graph where state flows between nodes, gets transformed, and potentially diverges into parallel branches. When something goes wrong, the failure could be in any of these locations:
- The state object passing between nodes
- The conditional edge logic that determines navigation
- The underlying LLM API calls (where HolySheep AI provides sub-50ms latency with 99.9% uptime)
- The checkpointing mechanism that manages state persistence
This tutorial assumes you have a basic LangGraph project running. We'll use HolySheep AI as our API provider for LLM calls—they offer GPT-4.1 at $8/MTok, Claude Sonnet 4.5 at $15/MTok, and DeepSeek V3.2 at just $0.42/MTok, with rates as low as ¥1=$1 (85%+ savings compared to typical ¥7.3 rates).
Setting Up Comprehensive Logging Infrastructure
Before diving into debugging techniques, let's establish a proper logging foundation. The difference between a 5-minute debugging session and a 5-hour one often comes down to having the right logs in place.
Environment Configuration
# Install required dependencies
pip install langgraph langgraph-cli langsmith-sdk python-dotenv
Create .env file with HolySheep AI credentials
cat > .env << 'EOF'
HolySheep AI Configuration
Sign up at https://www.holysheep.ai/register for free credits
HOLYSHEEP_API_KEY=YOUR_HOLYSHEEP_API_KEY
HOLYSHEEP_BASE_URL=https://api.holysheep.ai/v1
Optional: LangSmith for enhanced tracing (free tier available)
LANGSMITH_TRACING=true
LANGSMITH_API_KEY=your_langsmith_key
LANGSMITH_PROJECT=langgraph-debugging
Debug flags
DEBUG_MODE=true
LOG_LEVEL=DEBUG
EOF
echo "Environment file created successfully"
Structured Logging Setup
import logging
import json
import functools
from typing import Any, Dict, Optional
from datetime import datetime
from pathlib import Path
Configure structured logging with rotation
log_dir = Path("logs")
log_dir.mkdir(exist_ok=True)
logging.basicConfig(
level=logging.DEBUG,
format='%(asctime)s | %(levelname)-8s | %(name)s | %(message)s',
handlers=[
logging.FileHandler(log_dir / f"langgraph_{datetime.now():%Y%m%d}.log"),
logging.StreamHandler()
]
)
logger = logging.getLogger("langgraph_debug")
class StateLogger:
"""Captures state transitions for visual debugging."""
def __init__(self, session_id: str):
self.session_id = session_id
self.transitions = []
self.state_snapshots = {}
def log_state_snapshot(self, node_name: str, state: Dict[str, Any],
metadata: Optional[Dict] = None):
"""Record a complete state snapshot at a node."""
snapshot = {
"timestamp": datetime.now().isoformat(),
"node": node_name,
"session_id": self.session_id,
"state_keys": list(state.keys()),
"state_preview": {k: self._truncate(v, 200) for k, v in state.items()},
"metadata": metadata or {}
}
# Track state changes for diff analysis
if self.state_snapshots:
prev_state = self.state_snapshots[max(self.state_snapshots.keys())]
snapshot["state_diff"] = self._compute_diff(prev_state, state)
self.state_snapshots[node_name] = state.copy()
self.transitions.append(snapshot)
# Log with context for easy grep filtering
logger.debug(f"[STATE_SNAPSHOT] Node: {node_name} | Keys: {snapshot['state_keys']}")
if "state_diff" in snapshot:
logger.debug(f"[STATE_DIFF] {snapshot['state_diff']}")
def _truncate(self, value: Any, max_len: int) -> Any:
"""Truncate long values for logging."""
if isinstance(value, str) and len(value) > max_len:
return value[:max_len] + "..."
if isinstance(value, list) and len(value) > 10:
return f"[{len(value)} items]"
return value
def _compute_diff(self, prev: Dict, curr: Dict) -> Dict[str, str]:
"""Compute simple state differences."""
diff = {}
all_keys = set(prev.keys()) | set(curr.keys())
for key in all_keys:
if key not in prev:
diff[key] = f"NEW: {self._truncate(curr[key], 100)}"
elif key not in curr:
diff[key] = "REMOVED"
elif prev[key] != curr[key]:
diff[key] = f"CHANGED: {self._truncate(prev[key], 50)} → {self._truncate(curr[key], 50)}"
return diff
def export_debug_report(self, filepath: str = "debug_report.json"):
"""Export complete debugging report."""
report = {
"session_id": self.session_id,
"generated_at": datetime.now().isoformat(),
"total_transitions": len(self.transitions),
"transitions": self.transitions
}
with open(filepath, 'w') as f:
json.dump(report, f, indent=2, default=str)
logger.info(f"Debug report exported to {filepath}")
return filepath
Decorator for automatic node logging
def log_node_execution(logger_instance: logging.Logger):
"""Decorator to automatically log node execution details."""
def decorator(func):
@functools.wraps(func)
def wrapper(state: Dict[str, Any], config: Optional[Dict] = None):
node_name = func.__name__
logger_instance.debug(f"[NODE_ENTER] {node_name}")
logger_instance.debug(f"[INPUT_STATE] {json.dumps(state, default=str, indent=2)}")
try:
result = func(state, config)
logger_instance.debug(f"[NODE_EXIT] {node_name} - SUCCESS")
return result
except Exception as e:
logger_instance.error(f"[NODE_EXIT] {node_name} - FAILED: {type(e).__name__}: {str(e)}")
logger_instance.error(f"[NODE_STATE_AT_FAILURE] {json.dumps(state, default=str)}")
raise
return wrapper
return decorator
Usage example:
@log_node_execution(logger)
def my_node(state: State) -> State:
# Your node logic here
pass
Building a Visual Debugger for LangGraph Execution
Text logs are helpful, but nothing beats visual debugging when you're trying to understand complex state flows. Let's build a complete visualization system that generates execution traces you can inspect in real-time.
Real-Time Execution Visualizer
import asyncio
from typing import TypedDict, List, Annotated, Sequence
from langgraph.graph import StateGraph, END
from langchain_openai import ChatOpenAI
from langchain_core.messages import BaseMessage, HumanMessage, AIMessage
from dataclasses import dataclass, field
from enum import Enum
import time
HolySheep AI LLM setup
def get_holysheep_llm(model: str = "gpt-4.1"):
"""Initialize LLM with HolySheep AI backend.
HolySheep AI offers:
- GPT-4.1: $8/MTok (vs $15 standard)
- Claude Sonnet 4.5: $15/MTok
- DeepSeek V3.2: $0.42/MTok (budget-friendly)
- Gemini 2.5 Flash: $2.50/MTok
- Sub-50ms latency guarantee
- WeChat/Alipay payment support
"""
return ChatOpenAI(
model=model,
api_key="YOUR_HOLYSHEEP_API_KEY", # Replace with your HolySheep key
base_url="https://api.holysheep.ai/v1",
timeout=30,
max_retries=3
)
class ExecutionStatus(Enum):
PENDING = "pending"
RUNNING = "running"
SUCCESS = "success"
FAILED = "failed"
RETRYING = "retrying"
@dataclass
class ExecutionNode:
"""Represents a single node execution in the graph."""
name: str
status: ExecutionStatus = ExecutionStatus.PENDING
start_time: float = 0
end_time: float = 0
input_state: dict = field(default_factory=dict)
output_state: dict = field(default_factory=dict)
error: str = ""
retry_count: int = 0
llm_calls: List[dict] = field(default_factory=list)
class VisualDebugGraph:
"""Complete debugging system for LangGraph with visual trace generation."""
def __init__(self, session_id: str):
self.session_id = session_id
self.execution_trace: List[ExecutionNode] = []
self.state_history: List[dict] = []
self.event_log: List[dict] = []
self.logger = logging.getLogger(f"debug.{session_id}")
def create_debugging_graph(self) -> StateGraph:
"""Create a LangGraph with built-in debugging hooks."""
# Define state schema
class GraphState(TypedDict):
messages: Annotated[Sequence[BaseMessage], add_messages]
query: str
context: dict
step_count: int
debug_info: dict
def add_messages(existing: List, new: List) -> List:
return existing + new
# Node definitions with debugging hooks
def understanding_node(state: GraphState) -> GraphState:
"""Analyzes user query with detailed tracing."""
node = ExecutionNode(name="understanding", input_state=state.copy())
self.execution_trace.append(node)
node.status = ExecutionStatus.RUNNING
node.start_time = time.time()
self.logger.info(f"[UNDERSTANDING] Processing query: {state.get('query', '')[:100]}")
try:
llm = get_holysheep_llm("gpt-4.1")
# Track LLM call details
call_start = time.time()
understanding_prompt = f"""Analyze this query and extract:
1. Intent: What does the user want?
2. Entities: Key information mentioned
3. Constraints: Any limitations or requirements
Query: {state.get('query', '')}"""
response = llm.invoke([
HumanMessage(content=understanding_prompt)
])
call_duration = (time.time() - call_start) * 1000 # ms
node.llm_calls.append({
"model": "gpt-4.1",
"provider": "HolySheep AI",
"latency_ms": round(call_duration, 2),
"prompt_tokens": "tracked_by_provider",
"completion_tokens": "tracked_by_provider"
})
self.logger.debug(f"[LLM_CALL] gpt-4.1 completed in {call_duration:.2f}ms")
new_state = {
**state,
"context": {
"intent": response.content[:200],
"raw_response": response.content
},
"step_count": state.get("step_count", 0) + 1
}
node.output_state = new_state.copy()
node.status = ExecutionStatus.SUCCESS
node.end_time = time.time()
return new_state
except Exception as e:
node.status = ExecutionStatus.FAILED
node.error = f"{type(e).__name__}: {str(e)}"
node.end_time = time.time()
self.logger.error(f"[NODE_FAILURE] understanding: {node.error}")
raise
def reasoning_node(state: GraphState) -> GraphState:
"""Performs multi-step reasoning with checkpointing."""
node = ExecutionNode(name="reasoning", input_state=state.copy())
self.execution_trace.append(node)
node.status = ExecutionStatus.RUNNING
node.start_time = time.time()
try:
llm = get_holysheep_llm("deepseek-v3.2") # Cost-effective model
reasoning_prompt = f"""Based on the understanding:
{state.get('context', {}).get('intent', '')}
Generate a structured reasoning chain with:
1. Premise analysis
2. Logical steps
3. Conclusion
4. Confidence level (0-1)"""
response = llm.invoke([
HumanMessage(content=reasoning_prompt)
])
node.llm_calls.append({
"model": "deepseek-v3.2",
"provider": "HolySheep AI",
"cost_per_mtok": 0.42 # HolySheep pricing
})
new_state = {
**state,
"context": {
**state.get("context", {}),
"reasoning": response.content,
"confidence": 0.85 # Example extraction
},
"step_count": state.get("step_count", 0) + 1
}
node.output_state = new_state.copy()
node.status = ExecutionStatus.SUCCESS
node.end_time = time.time()
return new_state
except Exception as e:
node.status = ExecutionStatus.FAILED
node.error = str(e)
node.end_time = time.time()
raise
def response_node(state: GraphState) -> GraphState:
"""Generates final response."""
node = ExecutionNode(name="response", input_state=state.copy())
self.execution_trace.append(node)
node.start_time = time.time()
node.status = ExecutionStatus.RUNNING
try:
llm = get_holysheep_llm("gpt-4.1")
response_prompt = f"""Context: {state.get('context', {})}
Generate a clear, helpful response."""
response = llm.invoke([
HumanMessage(content=response_prompt)
])
new_state = {
**state,
"messages": state.get("messages", []) + [AIMessage(content=response.content)],
"final_response": response.content,
"step_count": state.get("step_count", 0) + 1
}
node.output_state = new_state.copy()
node.status = ExecutionStatus.SUCCESS
node.end_time = time.time()
return new_state
except Exception as e:
node.status = ExecutionStatus.FAILED
node.error = str(e)
node.end_time = time.time()
raise
# Build graph
workflow = StateGraph(GraphState)
workflow.add_node("understanding", understanding_node)
workflow.add_node("reasoning", reasoning_node)
workflow.add_node("response", response_node)
workflow.set_entry_point("understanding")
workflow.add_edge("understanding", "reasoning")
workflow.add_edge("reasoning", "response")
workflow.add_edge("response", END)
return workflow.compile()
def generate_execution_report(self) -> dict:
"""Generate comprehensive execution report."""
total_time = sum(
(n.end_time - n.start_time)
for n in self.execution_trace
if n.end_time > 0
)
total_llm_cost = sum(
sum(call.get("cost_per_mtok", 0) for call in n.llm_calls)
for n in self.execution_trace
)
report = {
"session_id": self.session_id,
"total_execution_time_ms": round(total_time * 1000, 2),
"nodes_executed": len(self.execution_trace),
"nodes_by_status": {
status.value: sum(1 for n in self.execution_trace if n.status.value == status.value)
for status in ExecutionStatus
},
"total_llm_calls": sum(len(n.llm_calls) for n in self.execution_trace),
"estimated_cost_usd": round(total_llm_cost, 4),
"execution_details": [
{
"node": n.name,
"status": n.status.value,
"duration_ms": round((n.end_time - n.start_time) * 1000, 2) if n.end_time > 0 else 0,
"llm_calls": n.llm_calls,
"error": n.error if n.error else None
}
for n in self.execution_trace
]
}
return report
Run the debugging session
async def run_debug_session():
debugger = VisualDebugGraph(session_id=f"debug_{int(time.time())}")
print("🚀 Starting debug session with visual tracing...")
try:
graph = debugger.create_debugging_graph()
initial_state = {
"messages": [HumanMessage(content="What is LangGraph and how do I debug it?")],
"query": "What is LangGraph and how do I debug it?",
"context": {},
"step_count": 0,
"debug_info": {"session": debugger.session_id}
}
# Execute with async for real-time streaming
result = await graph.ainvoke(initial_state)
print("\n" + "="*60)
print("📊 EXECUTION REPORT")
print("="*60)
report = debugger.generate_execution_report()
print(json.dumps(report, indent=2, default=str))
return result
except Exception as e:
print(f"\n❌ Execution failed: {type(e).__name__}: {str(e)}")
print("\n📋 Debugger captured the following trace:")
for node in debugger.execution_trace:
print(f"\n Node: {node.name}")
print(f" Status: {node.status.value}")
print(f" Error: {node.error}")
print(f" Input: {json.dumps(node.input_state, default=str)[:200]}")
raise
Execute (run with: python your_script.py)
asyncio.run(run_debug_session())
Error Scenario: Handling ConnectionError and Timeout Failures
Let me walk you through a real error scenario I encountered last month. Our LangGraph agent was processing customer support requests, and suddenly every LLM call started failing with ConnectionError: timeout. The error message was cryptic, but here's the complete troubleshooting workflow I developed.
Scenario: LLM Provider Timeout Under Load
During peak traffic (approximately 2,000 requests/minute), the HolySheep AI API responds in under 50ms normally, but our graph would occasionally hit the 30-second timeout threshold during cold starts. The solution involved implementing intelligent retry logic with exponential backoff and fallback model selection.
import asyncio
import backoff
from typing import Optional, Callable, Any
from datetime import datetime, timedelta
import random
class HolySheepErrorHandler:
"""Production-grade error handling for HolySheep API calls."""
# Define retry configuration
RETRY_CONFIG = {
"max_retries": 4,
"base_delay": 1.0, # seconds
"max_delay": 30.0,
"exponential_base": 2,
"jitter": True
}
# Model fallback hierarchy (cheapest first for cost optimization)
MODEL_FALLBACKS = {
"gpt-4.1": ["gpt-4.1", "claude-sonnet-4.5", "gemini-2.5-flash", "deepseek-v3.2"],
"claude-sonnet-4.5": ["claude-sonnet-4.5", "gemini-2.5-flash", "deepseek-v3.2"],
"deepseek-v3.2": ["deepseek-v3.2"], # Already most cost-effective
}
def __init__(self, api_key: str):
self.api_key = api_key
self.error_counts = {}
self.last_success = {}
self.circuit_breaker_state = {} # per-model circuit breaker
def get_backoff_delay(self, attempt: int) -> float:
"""Calculate delay with exponential backoff and jitter."""
delay = self.RETRY_CONFIG["base_delay"] * (
self.RETRY_CONFIG["exponential_base"] ** attempt
)
delay = min(delay, self.RETRY_CONFIG["max_delay"])
if self.RETRY_CONFIG["jitter"]:
delay *= (0.5 + random.random()) # 50-150% of calculated delay
return delay
def should_retry(self, error: Exception, attempt: int) -> bool:
"""Determine if error is retryable."""
retryable_errors = (
ConnectionError,
TimeoutError,
asyncio.TimeoutError
)
# Retry on connection/timeout errors
if isinstance(error, retryable_errors):
return True
# Check for rate limit (429) or server errors (5xx)
error_str = str(error).lower()
if "429" in error_str or "500" in error_str or "502" in error_str or "503" in error_str:
return True
# Don't retry on auth errors or client errors (4xx except 429)
if "401" in error_str or "403" in error_str or "400" in error_str:
return False
return attempt < self.RETRY_CONFIG["max_retries"]
def record_error(self, model: str, error: Exception):
"""Track error frequency for circuit breaking."""
self.error_counts[model] = self.error_counts.get(model, 0) + 1
# Open circuit breaker after 5 consecutive failures
if self.error_counts.get(model, 0) >= 5:
self.circuit_breaker_state[model] = {
"open_since": datetime.now(),
"failure_count": self.error_counts[model]
}
print(f"⚠️ Circuit breaker OPEN for {model} due to {self.error_counts[model]} failures")
def record_success(self, model: str):
"""Record successful call and reset error tracking."""
self.error_counts[model] = 0
self.last_success[model] = datetime.now()
# Close circuit breaker after successful call
if model in self.circuit_breaker_state:
del self.circuit_breaker_state[model]
print(f"✅ Circuit breaker CLOSED for {model}")
def is_circuit_open(self, model: str) -> bool:
"""Check if circuit breaker is open."""
if model not in self.circuit_breaker_state:
return False
cb_info = self.circuit_breaker_state[model]
# Allow half-open state after 30 seconds
if datetime.now() - cb_info["open_since"] > timedelta(seconds=30):
return False # Try half-open
return True
async def call_with_fallback(
self,
user_function: Callable,
primary_model: str,
*args,
**kwargs
) -> Any:
"""Execute function with automatic retry and model fallback."""
available_models = self.MODEL_FALLBACKS.get(primary_model, [primary_model])
last_error = None
for attempt in range(self.RETRY_CONFIG["max_retries"] + 1):
for model in available_models:
# Skip if circuit breaker is open
if self.is_circuit_open(model):
print(f"⏭️ Skipping {model} - circuit breaker open")
continue
try:
# Inject model into kwargs if needed
if 'model' in kwargs or 'model_name' in kwargs:
kwargs['model'] = model
print(f"🔄 Attempt {attempt + 1}: Calling with {model}")
result = await asyncio.wait_for(
user_function(*args, **kwargs),
timeout=30.0
)
self.record_success(model)
return result
except asyncio.TimeoutError:
error_msg = f"Timeout calling {model}"
print(f"⏱️ {error_msg}")
self.record_error(model, asyncio.TimeoutError())
last_error = asyncio.TimeoutError(error_msg)
break # Try next model
except ConnectionError as e:
error_msg = f"ConnectionError calling {model}: {str(e)}"
print(f"🔌 {error_msg}")
self.record_error(model, e)
last_error = e
# Apply backoff before retry
if attempt < self.RETRY_CONFIG["max_retries"]:
delay = self.get_backoff_delay(attempt)
print(f"⏳ Waiting {delay:.2f}s before retry...")
await asyncio.sleep(delay)
except Exception as e:
print(f"❌ Unexpected error with {model}: {type(e).__name__}: {str(e)}")
self.record_error(model, e)
last_error = e
break # Try next model
# All models and retries exhausted
raise RuntimeError(
f"All retry attempts failed. Last error: {type(last_error).__name__}: {str(last_error)}"
) from last_error
Example usage in a LangGraph node
async def robust_llm_node(state: dict, error_handler: HolySheepErrorHandler):
"""Example node with robust error handling."""
llm = ChatOpenAI(
model="gpt-4.1",
api_key="YOUR_HOLYSHEEP_API_KEY",
base_url="https://api.holysheep.ai/v1"
)
async def llm_call():
return await llm.ainvoke([
HumanMessage(content=state.get("query", ""))
])
try:
response = await error_handler.call_with_fallback(
llm_call,
primary_model="gpt-4.1"
)
return {"response": response.content, "status": "success"}
except Exception as e:
print(f"🚨 All fallback attempts exhausted: {e}")
return {"response": None, "status": "failed", "error": str(e)}
Test the error handler
async def test_error_handling():
handler = HolySheepErrorHandler("YOUR_HOLYSHEEP_API_KEY")
# Simulate some calls
for i in range(5):
try:
result = await handler.call_with_fallback(
lambda: asyncio.sleep(0.1) or {"status": "ok"},
primary_model="deepseek-v3.2"
)
print(f"Call {i+1}: {result}")
except Exception as e:
print(f"Call {i+1} failed: {e}")
asyncio.run(test_error_handling())
Implementing State Inspection and Breakpoints
One of the most powerful debugging techniques is being able to pause execution and inspect state at any point in your graph. Let's implement a sophisticated breakpoint system.
from typing import Any, Dict, Callable, Optional
from dataclasses import dataclass
from enum import Enum
import ipdb # Interactive debugger - install with: pip install ipdb
class BreakpointType(Enum):
BEFORE_NODE = "before_node"
AFTER_NODE = "after_node"
ON_CONDITION = "on_condition"
ON_ERROR = "on_error"
@dataclass
class Breakpoint:
"""Defines a debugging breakpoint."""
bp_type: BreakpointType
target: str # Node name, condition function, or "any"
callback: Optional[Callable] = None
condition: Optional[Callable[[Dict], bool]] = None
auto_continue: bool = False # If True, don't actually pause
class StateBreakpointDebugger:
"""Interactive debugger for LangGraph state inspection."""
def __init__(self):
self.breakpoints: Dict[str, Breakpoint] = {}
self.watch_variables: list = []
self.execution_history: list = []
self.is_active = True
def set_breakpoint(
self,
node_name: str,
bp_type: BreakpointType = BreakpointType.BEFORE_NODE,
condition: Optional[Callable[[Dict], bool]] = None,
callback: Optional[Callable] = None
):
"""Register a breakpoint at a specific node."""
bp_id = f"{bp_type.value}:{node_name}"
self.breakpoints[bp_id] = Breakpoint(
bp_type=bp_type,
target=node_name,
condition=condition,
callback=callback
)
print(f"🔴 Breakpoint set: {bp_id}")
def watch(self, *variable_names: str):
"""Watch specific state variables for changes."""
self.watch_variables.extend(variable_names)
print(f"👁️ Watching variables: {variable_names}")
def _inspect_state(self, node_name: str, state: Dict[str, Any],
context: str = "entry"):
"""Display state for debugging."""
print(f"\n{'='*60}")
print(f"🔍 STATE INSPECTION at {node_name} ({context})")
print(f"{'='*60}")
# Show watched variables if any
if self.watch_variables:
print("\n📌 Watched Variables:")
for var in self.watch_variables:
value = state.get(var, "")
print(f" {var}: {value}")
# Show full state with formatting
print("\n📋 Complete State:")
for key, value in state.items():
display = str(value)
if len(display) > 200:
display = display[:200] + "..."
marker = "👁️" if key in self.watch_variables else " "
print(f" {marker} {key}: {display}")
# Show state size
state_size = len(str(state))
print(f"\n💾 State size: {state_size:,} bytes")
self.execution_history.append({
"node": node_name,
"context": context,
"state_keys": list(state.keys()),
"timestamp": datetime.now().isoformat()
})
def _handle_breakpoint(self, bp: Breakpoint, state: Dict[str, Any]) -> bool:
"""Process breakpoint hit."""
# Check condition
if bp.condition and not bp.condition(state):
return True # Continue execution
# Run callback if provided
if bp.callback:
bp.callback(state)
# Auto-continue mode
if bp.auto_continue:
print(f"⏩ Auto-continue enabled, skipping pause")
return True
# Interactive debugging with ipdb
print(f"\n🛑 Breakpoint hit: {bp.bp_type.value} at {bp.target}")
# Offer options
print("\nOptions:")
print(" c - Continue")
print(" n - Next (run until next breakpoint)")
print(" s - Step into")
print(" p - Print variable")
print(" w - Show watched variables")
print(" h - Show full state")
print(" q - Quit session")
# In production, you might use ipdb.set_trace() here
# For automated testing, we just continue
return True # Continue execution
def wrap_node(self, node_func: Callable, node_name: str) -> Callable:
"""Wrap a node function with breakpoint capabilities."""
async def wrapped_node(state: Dict[str, Any], config: Optional[Dict] = None):
# Check for BEFORE_NODE breakpoints
bp_id = f"{BreakpointType.BEFORE_NODE.value}:{node_name}"
if bp_id in self.breakpoints:
self._inspect_state(node_name, state, "before")
self._handle_breakpoint(self.breakpoints[bp_id], state)
# Execute node
try:
result = await node_func(state, config)
# Check for AFTER_NODE breakpoints
bp_id_after = f"{BreakpointType.AFTER_NODE.value}:{node_name}"
if bp_id_after in self.breakpoints:
self._inspect_state(node_name, result, "after")
self._handle_breakpoint(self.breakpoints[bp_id_after], result)
return result
except Exception as e:
# Check for ON_ERROR breakpoints
bp_id_error = f"{BreakpointType.ON_ERROR.value}:any"
if bp_id_error in self.breakpoints:
self._inspect_state(node_name, state, "error")
print(f"\n💥 Error caught: {type(e).__name__}: {str(e)}")
self._handle_breakpoint(self.breakpoints[bp_id_error], state)
raise
return wrapped_node
def generate_debug_report(self) -> Dict[str, Any]:
"""Generate comprehensive debugging report."""
return {
"total_nodes_visited": len(self.execution_history),
"nodes": self.execution_history,
"active_breakpoints": len(self.breakpoints),
"breakpoint_details": [
{
"id": bp_id,
"type": bp.bp_type.value,
"target": bp.target,
"has_condition": bp.condition is not None,
"auto_continue": bp.auto_continue
}
for bp_id, bp in self.breakpoints.items()
],
"watched_variables": self.watch_variables
}
Usage example
def create_debugged_graph():
"""Example: Creating a graph with debugging enabled."""
debugger = StateBreakpointDebugger()
# Set breakpoints
debugger.set_breakpoint(
"understanding",
BreakpointType.BEFORE_NODE,
callback=lambda s: print(f"📥 Query received: {s.get('query', '')[:50]}")
)
debugger.watch("messages", "step_count", "context")
# Create your graph
class State(TypedDict):
messages: List
query: str
step_count: int
context: dict
def understanding(state: State) -> State:
return {**state, "step_count": 1, "context": {"understood": True}}
def reasoning(state: State) -> State:
return {**state, "step_count": 2, "context": {"reasoned": True}}
workflow = StateGraph(State)
workflow.add_node("understanding", debugger.wrap_node(understanding, "understanding"))
workflow.add_node("reasoning", debugger.wrap_node(reasoning, "reasoning"))
workflow.set_entry_point("understanding")
workflow.add_edge("understanding", "reasoning")
workflow.add_edge("reasoning", END)
return workflow.compile(), debugger
Run debugged graph