Skip to content

LangGraph Compatibility

This tutorial validates ReplayLab with a LangGraph StateGraph workflow. It is a compatibility scenario, not a native LangGraph adapter.

The goal is to prove that provider calls made inside graph nodes can still be captured, replayed, compared, viewed, and converted into pytest provider replay guards through the normal ReplayLab workflow. ReplayLab also has a focused LangGraph ToolNode scenario that records execution-tool evidence without requiring manual trace_tool wrappers.

Why This Matters

Graph frameworks often hide provider calls behind nodes, compiled graphs, and invoke(...) calls. ReplayLab should not need to understand the graph runtime to capture supported provider boundaries. It only needs startup provider instrumentation and a capture scope around the graph invocation.

This scenario follows the documented LangGraph shape: define a StateGraph, add nodes, connect START and END, compile the graph, and run graph.invoke(...). The separate langgraph-tool-local scenario validates normal ToolNode dispatch with bound LangChain tools.

Run The Scenario

Run:

python scripts/run_scenario.py run langgraph-local --keep-workspace

To validate ToolNode execution evidence, run:

python scripts/run_scenario.py run langgraph-tool-local --keep-workspace

Expected ending:

ReplayLab scenario passed.
Scenario: langgraph-local
Tier: loopback
Boundaries: 2
Providers: requests, openai

The ToolNode scenario ends with:

ReplayLab scenario passed.
Scenario: langgraph-tool-local
Tier: loopback
Boundaries: 3
Providers: openai, execution_tool

ReplayLab creates a clean temporary virtual environment, installs the current checkout plus langgraph, requests, openai, and pytest, starts loopback HTTP and OpenAI Responses endpoints only for capture, then stops both endpoints before replay and generated pytest.

App Shape

The generated scenario app uses startup instrumentation and normal provider calls inside graph nodes:

import openai
import replaylab
import requests
from langgraph.graph import END, START, StateGraph
from replaylab import CapturePayloadPolicy

handle = replaylab.init(
    project_name="langgraph-local",
    auto_patch_integrations="auto",
    capture_payload_policy=CapturePayloadPolicy.FULL,
)

def fetch_ticket(state):
    response = requests.get(f"{HTTP_BASE_URL}/provider/tickets/{state['ticket_id']}", timeout=5)
    response.raise_for_status()
    return {**state, "provider": response.json()["provider"]}

def classify_ticket(state):
    client = openai.OpenAI(base_url=f"{OPENAI_BASE_URL}/v1", api_key="scenario-key")
    response = client.responses.create(
        model="gpt-5-mini",
        input=f"Classify ticket {state['ticket_id']} from {state['provider']}.",
    )
    return {**state, "priority": response.output_text.strip()}

graph = StateGraph(dict)
graph.add_node("fetch_ticket", fetch_ticket)
graph.add_node("classify_ticket", classify_ticket)
graph.add_edge(START, "fetch_ticket")
graph.add_edge("fetch_ticket", "classify_ticket")
graph.add_edge("classify_ticket", END)
compiled = graph.compile()

with handle.capture("langgraph_ticket_graph"):
    result = compiled.invoke({"ticket_id": "123", "provider": "", "priority": ""})

ReplayLab captures the provider calls made by the node functions. The graph itself stays normal. When safe callsite metadata is available, the local app groups the provider boundaries under inferred graph nodes such as Graph node fetch_ticket and Graph node classify_ticket. The breadcrumbs store module/function-style identifiers only; they do not store raw arguments, local variables, source bodies, raw command arguments, secrets, or absolute paths.

For ToolNode dispatch, initialize ReplayLab with auto_patch_integrations=("openai", "langchain", "langgraph"). ReplayLab records the LangGraph dispatch as one execution_tool boundary with source=langgraph_framework and suppresses nested LangChain evidence for the same tool call.

What ReplayLab Captures

The scenario expects two full-payload provider boundaries in sequence:

1. provider=requests operation=GET
2. provider=openai resource=openai.responses

The ToolNode scenario expects three boundaries:

1. provider=openai resource=openai.responses
2. provider=execution_tool resource=lookup_customer source=langgraph_framework
3. provider=openai resource=openai.responses

Replay succeeds with both loopback providers stopped. That proves the graph invocation is served from the capsule rather than calling live dependencies.

In replaylab app, a provider-backed LangGraph trace should show:

Project run
  Workflow step
    Graph node fetch_ticket
      API call
    Graph node classify_ticket
      LLM call

Provider-free LangGraph runs are different. ReplayLab may still record run/step metadata, but the local app labels those captures as No replayable evidence and disables replay, live experiment, and generated-guard actions until the workflow crosses a supported provider boundary.

What Is Not Yet Supported

  • LangChain ChatOpenAI or Chat Completions model wrappers.
  • Streaming graph/model calls.
  • LangGraph-native semantic trace visualization or graph-edge replay semantics beyond provider-backed inferred grouping and supported ToolNode dispatch evidence.
  • Framework-specific node or edge replay semantics beyond provider boundary replay.

Those remain future work. The current guarantee is provider-level capture/replay for supported requests and OpenAI Responses calls inside LangGraph nodes.