Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.reasonblocks.com/llms.txt

Use this file to discover all available pages before exploring further.

LangChain 1.0’s create_agent is built on LangGraph — it returns a CompiledStateGraph you invoke like any other graph. So if you’re already using create_agent, the LangChain guide and rb.middleware() apply unchanged: the middleware hooks before_model / wrap_model_call on the underlying graph runtime. This page is for the second shape: hand-rolling your own StateGraph. There’s no AgentMiddleware slot to plug into when you’re defining nodes and edges yourself, so you wire a SteeringSession into the graph by hand. Same pipeline, same telemetry, same monitor + E-trace + routing behavior — just expressed as graph nodes instead of middleware hooks.

Path 1: create_agent (LangGraph under the hood)

If you build your agent with create_agent, you don’t need a LangGraph-specific integration. The middleware works as documented in the LangChain guide:
from langchain.agents import create_agent
from reasonblocks import ReasonBlocks

rb = ReasonBlocks(api_key="rb_live_...")

with rb.middleware(agent_name="reviewer", task="...") as mw:
    agent = create_agent(
        model="anthropic:claude-haiku-4-5-20251001",
        tools=[...],
        middleware=[mw],
    )
    result = agent.invoke({"messages": [...]})
The returned graph is a langgraph.graph.state.CompiledStateGraph. Every step runs through the FSM scorer, monitor evaluator, E-trace pipeline, and live telemetry emitter — same as a non-LangGraph LangChain agent.

Path 2: hand-rolled StateGraph

When you build a graph from scratch with langgraph.graph.StateGraph, you call the model from your own node function. Wire SteeringSession around that node so the pipeline runs on every model call.
1

Install

pip install reasonblocks "langgraph>=0.2" langchain-anthropic
2

Build the steering session

rb.claude_messages_session(...) builds a session wired against an Anthropic model identifier. rb.middleware(...).session does the same against any LangChain init_chat_model identifier — but for hand-rolled graphs without create_agent, the cleaner path is to construct SteeringSession directly so you can choose your own framework label.
import os
from reasonblocks import (
    DifficultyFSM, ReasonBlocks, SteeringSession, TraceStateManager,
)
from reasonblocks.api import ReasonBlocksAPI
from reasonblocks.injections import create_injections
from reasonblocks.monitors import create_monitors
from reasonblocks.streaming_emitter import StreamingEmitter
from reasonblocks.monitor_client import MonitorClient

rb = ReasonBlocks(api_key=os.environ["REASONBLOCKS_API_KEY"])

api = ReasonBlocksAPI(api_key=rb._api_key)
state_manager = TraceStateManager()
session = SteeringSession(
    score_fn=rb.score_step,
    fsm=DifficultyFSM(),
    state_manager=state_manager,
    injections=create_injections(api, create_monitors(None)),
    model_routing=rb._model_routing or None,
    emitter=StreamingEmitter(MonitorClient(api_key=rb._api_key)),
    run_id=state_manager.trace_id,
    run_metadata={
        "agent_name": "reviewer", "task": "...",
        "framework": "langgraph", "task_profile": "coding",
    },
)
Most users won’t build a session this manually — call rb.claude_messages_session(...) (for Claude) or wrap with rb.openai_model(...) (for OpenAI) and let those factories assemble the pieces. The hand-built form is shown here because pure-StateGraph users typically pick their own model adapter.
3

Define your graph nodes

Two node helpers — one before the LLM call, one after — keep the steering pipeline orthogonal to the rest of your graph.
from typing import TypedDict, Annotated, Any
from langchain_anthropic import ChatAnthropic
from langchain.messages import AIMessage, HumanMessage, SystemMessage
from langgraph.graph import StateGraph, END
from langgraph.graph.message import add_messages
import time

class GraphState(TypedDict):
    messages: Annotated[list, add_messages]
    system_prompt: str
    # Carries the StepDecision between pre and post nodes.
    _rb_decision: Any

def steering_pre(state: GraphState) -> dict:
    # Last assistant content is the "thought" for scoring.
    thought = None
    for m in reversed(state["messages"]):
        if isinstance(m, AIMessage) and m.content:
            thought = m.content if isinstance(m.content, str) else " ".join(
                b.get("text", "") if isinstance(b, dict) else str(b)
                for b in m.content
            )
            break
    decision = session.begin_step(thought=thought)
    return {"_rb_decision": decision}

_model_cache: dict[str, Any] = {}
def _resolve_model(model_id: str) -> Any:
    if model_id not in _model_cache:
        _model_cache[model_id] = ChatAnthropic(model=model_id, max_tokens=2048)
    return _model_cache[model_id]

def llm_node(state: GraphState) -> dict:
    decision = state["_rb_decision"]
    system = decision.compose_system_prompt(state["system_prompt"])
    model_id = (
        decision.model_override.split(":", 1)[1]
        if decision.model_override and ":" in decision.model_override
        else decision.model_override or "claude-haiku-4-5-20251001"
    )
    model = _resolve_model(model_id)

    t0 = time.perf_counter()
    response = model.invoke([SystemMessage(content=system), *state["messages"]])
    elapsed_ms = (time.perf_counter() - t0) * 1000.0

    # Pull token usage off the response for the post node.
    tokens = 0
    usage = getattr(response, "usage_metadata", None)
    if isinstance(usage, dict):
        tokens = int(usage.get("total_tokens") or 0)
    tool_calls = [tc.get("name") for tc in (response.tool_calls or [])]

    return {
        "messages": [response],
        "_rb_meta": {
            "model_id": model_id, "tokens": tokens,
            "tool_calls": tool_calls, "latency_ms": elapsed_ms,
        },
    }

def steering_post(state: GraphState) -> dict:
    decision = state["_rb_decision"]
    meta = state.get("_rb_meta") or {}
    session.end_step(decision, **meta)
    return {}
4

Compose the graph + run

builder = StateGraph(GraphState)
builder.add_node("steering_pre", steering_pre)
builder.add_node("llm", llm_node)
builder.add_node("steering_post", steering_post)
builder.set_entry_point("steering_pre")
builder.add_edge("steering_pre", "llm")
builder.add_edge("llm", "steering_post")
builder.add_edge("steering_post", END)

graph = builder.compile()

with session:
    out = graph.invoke({
        "messages": [HumanMessage(content="What's wrong with MarkerWidget?")],
        "system_prompt": "You are a debugging assistant.",
        "_rb_decision": None,
    })
Looping (multi-step) graphs cycle through steering_prellmsteering_post → router → steering_pre again until the router decides the run is done. Each cycle produces one step_log entry on session.
5

Inspect the step log

for entry in session.step_log:
    print(entry.as_dict())
Same shape as mw.step_log from the LangChain middleware: difficulty, FSM state, monitors fired, injection text, model id used, tokens, and latency.

What the LangGraph integration shares with LangChain

Identical: FSM scoring, server-side monitor evaluation, E1/E2/E3 retrieval, model routing, telemetry emission. Both ultimately drive the same SteeringSession. When LangChain 1.0 calls your middleware’s before_model hook, it’s running on the LangGraph runtime — that’s why our existing tests cover both paths.

What you don’t get on the hand-rolled path

Token-saving compression and the general-monitor middleware are LangChain AgentMiddleware implementations. They’re plumbed through create_agent but don’t have a hand-rolled StateGraph analog yet. Use the rb.middleware() + create_agent path if you need those.

Codebase memory tools

make_langchain_tools returns @tool-decorated functions that work in any LangGraph node, including hand-rolled graphs:
from reasonblocks import CodebaseMemory, ImportGraph
from reasonblocks.integrations.langchain_tools import make_langchain_tools

memory = CodebaseMemory(codebase_id="my-org/my-repo", api_key=rb._api_key)
graph_tools = make_langchain_tools(memory)
Bind them to your model (ChatAnthropic(...).bind_tools(graph_tools)) or attach to a ToolNode. The contract is unchanged from the LangChain integration.

LangChain guide

Full middleware walkthrough for create_agent-based agents.

SteeringSession reference

The shared core driving every framework integration.