Skip to main content
GitHub

LangGraph

Auto-instrumentation for LangGraph agents.

Risicare provides deep integration with LangGraph for graph-based agent observability.

Version Compatibility

Requires langgraph >= 0.2.0 and langchain-core >= 0.2.0.

Installation

pip install 'risicare[langgraph]'
# or
pip install risicare langgraph

Basic Usage

import risicare
from langgraph.graph import StateGraph, END
 
risicare.init()
 
# Define your graph as usual - it's automatically traced
def agent_node(state):
    return {"messages": state["messages"] + ["Agent response"]}
 
graph = StateGraph(dict)
graph.add_node("agent", agent_node)
graph.set_entry_point("agent")
graph.add_edge("agent", END)
 
app = graph.compile()
result = app.invoke({"messages": ["Hello"]})

What's Captured

On the graph span

invoke, ainvoke, stream and astream on the compiled graph are wrapped. Each call produces one langgraph.graph/<name> span carrying:

AttributeMeaning
framework.langgraph.graph_nameCompiled graph name
framework.langgraph.state_keysKeys of the input state, on invoke and ainvoke, for a non-empty dict input
framework.langgraph.result_keysKeys of the final result state, on invoke and ainvoke, when content capture is on (trace_content=True)
framework.langgraph.thread_idThread ID, when config= is passed as a keyword and holds configurable.thread_id. A config passed by position (app.invoke(x, {...})) sets nothing
framework.langgraph.streaming / streamed_eventsSet on the stream and astream paths

Per-node spans

Each node execution produces its own span named after the node (langchain.chain/<node_name>), with timing, under a langchain.chain/LangGraph span below the graph span. These come from the LangChain integration rather than the LangGraph one, so they appear whenever langchain_core is also instrumented. A node that runs several times in a loop produces one span per execution.

Graph topology is not captured

Risicare records what the graph did, not how it is wired. There are no attributes for edge connections, entry/exit points, or node types, and no loop-iteration counter — repeated execution is visible only as repeated node spans, which you would have to count yourself. state_keys is recorded once from the input state, not at each step. If you need node-level state diffs or an explicit graph topology, Risicare does not provide them today.

Agent Identity

Annotate nodes with agent identity:

from risicare import agent_context
 
def researcher_node(state):
    with agent_context("researcher-001", agent_name="researcher", agent_role="specialist"):
        # Research logic
        return {"findings": research_results}
 
def writer_node(state):
    with agent_context("writer-001", agent_name="writer", agent_role="specialist"):
        # Writing logic
        return {"draft": written_content}

Decision Phases

Track Think/Decide/Act within nodes:

from risicare import SemanticPhase, phase_context
 
def planning_node(state):
    with phase_context(SemanticPhase.THINK):
        analysis = analyze_task(state["task"])
 
    with phase_context(SemanticPhase.DECIDE):
        plan = create_plan(analysis)
 
    return {"plan": plan}
 
def execution_node(state):
    with phase_context(SemanticPhase.ACT):
        result = execute_plan(state["plan"])
 
    return {"result": result}

Conditional Routing

Conditional edges are visible through the span hierarchy:

def should_continue(state):
    if state["iteration"] >= 3:
        return "end"
    return "continue"
 
graph.add_conditional_edges(
    "agent",
    should_continue,
    {"continue": "agent", "end": END}
)

Routing decisions are reflected in which node spans execute next. The span tree shows the path taken through the graph, so you can infer routing outcomes from the execution sequence.

Subgraphs

Nested subgraphs maintain trace hierarchy:

# Parent graph
parent = StateGraph(dict)
parent.add_node("child_graph", child_app)  # Subgraph as node
 
# The child graph's spans are in the parent's trace

The child graph's own langgraph.graph span is a child of the outer graph span and has no children of its own; the child's node spans are under the child's langchain.chain/LangGraph span, which is under the parent's node span.

Provider Spans

A node that calls a LangChain model gets one langchain.chat/{model} span: the provider call underneath it gets no second span. A node that calls a provider SDK directly (for example the openai client) gets that provider's span.

Visualization

View graph execution in the dashboard:

  • The trace page: the span waterfall and the timeline of the node executions

JavaScript / TypeScript

The JS SDK provides instrumentLangGraph() for LangGraph.js:

import { instrumentLangGraph } from 'risicare/langgraph';
 
const tracedGraph = instrumentLangGraph(compiledGraph);
const result = await tracedGraph.invoke(input);

Wraps invoke() and stream() methods with tracing spans.

Next Steps