Skip to main content

LangChain

The LangChain adapter wraps an AgentExecutor (or any Runnable that fires LangChain callbacks) so every tool invocation, LLM call, and chain step is recorded as a governance event. If you’re using LangGraph specifically, the LangGraph adapter is more direct. Use the LangChain adapter for classic AgentExecutor-based agents, custom Runnable pipelines, or when you need tool-call-level granularity that the LangGraph adapter doesn’t provide.

Install

pip install "proofrail[langchain]"
This installs langchain and langchain-core if not already present.
Supported LangChain versions: 0.2.x - 0.3.x. Install with a LangChain version in this range; the version constraint is pinned in sdk/pyproject.toml.

Quick start

import asyncio
import proofrail
from proofrail.langchain import govern

from langchain.agents import AgentExecutor, create_tool_calling_agent
from langchain_openai import ChatOpenAI

proofrail.init(api_key="prail_...")

# Build your agent as usual
llm = ChatOpenAI(model="gpt-4o")
agent = create_tool_calling_agent(llm, tools, prompt)
executor = AgentExecutor(agent=agent, tools=tools)

# Wrap with govern()
governed = govern(executor, chain_name="support-agent")

# Use governed exactly like the original executor
result = await governed.ainvoke({"input": "What's the status of order #1234?"})
governed has the same interface as the underlying executor. ainvoke and invoke both work; tool calls inside the executor are recorded automatically.

How it works

The adapter attaches an AsyncCallbackHandler to the executor’s config. This handler fires on:
  • on_tool_start — every tool call before it executes
  • on_tool_end — every tool call after it completes
  • on_tool_error — if the tool raised
  • on_chain_start / on_chain_end — top-level executor invocations
Tool calls are recorded as tool_call events with the tool name as action_name and the input as the payload. Tool results are attached as tool_result events. Unlike the LangGraph adapter (which records node-level state), the LangChain adapter records at tool-call granularity. If your agent calls search_web, then calculate_offer, then send_email, those are three separate events — and each one passes through the policy engine before executing.

What gets recorded

For each tool call:
  • tool_call event with tool name as action_name, tool input as payload, with the agent’s identity in agent_name
  • tool_result event with the tool’s output (truncated if very large)
  • tool_error event if the tool raised, with the exception type and message
For the executor itself:
  • chain_start / chain_end at the top level marking the executor invocation boundaries
Internal LangChain machinery (parser callbacks, LLM streaming events) is filtered out — only meaningful agent decisions and tool calls reach the audit trail.

Policy enforcement

Pre-execution policy checks happen at on_tool_start. If a tool call is denied:
from proofrail.exceptions import ActionDeniedError

try:
    result = await governed.ainvoke({"input": "Send a refund of $50,000 to..."})
except ActionDeniedError as exc:
    print(f"Tool blocked: {exc.policy_name}")
    print(f"Condition: {exc.condition}")
    print(f"Remediation: {exc.remediation}")
The executor halts at the denied tool call. Earlier tool calls in the same invocation are already in the audit trail. The agent’s chain-of-thought reasoning leading up to the denial is preserved in the executor’s intermediate steps. For require_approval decisions, the callback blocks until the approver responds. The agent appears to “pause” mid-execution — your await governed.ainvoke(...) is waiting for the human. On approval the tool runs; on denial or timeout it raises.

Identifying the agent

The adapter derives the agent_name for recorded events from the executor’s class name at wrap time — specifically type(agent_executor_or_chain).__name__. So a vanilla AgentExecutor shows up as "AgentExecutor" in the audit trail. There’s no govern() kwarg to override the agent name in this version. If you need a more descriptive label, two options:
  • Subclass AgentExecutor with a meaningful class name (class SupportAgent(AgentExecutor): ...), then wrap your subclass
  • Use the framework-agnostic proofrail.Chain directly and call record_agent_action(agent_name="support-agent-v2", ...) yourself for full control

Configuring the chain

governed = govern(
    executor,
    chain_name="customer-support",
    metadata={
        "team": "support",
        "version": "2.1.0",
        "environment": "production",
    },
)
chain_name appears in your dashboard and identifies the workflow type. metadata is arbitrary key/value pairs attached to every invocation by this governed executor.

Passing LangChain config

Standard LangChain config (other callbacks, run names, tags) is preserved:
result = await governed.ainvoke(
    {"input": "..."},
    config={
        "callbacks": [my_other_callback],
        "run_name": "batch-job-42",
        "tags": ["production", "tier-1"],
    },
)
ProofRail’s callback is appended to whatever you pass; your existing callbacks continue to fire normally.

Sync invocation

result = governed.invoke({"input": "..."})
Raises RuntimeError if called from inside a running event loop. In that case use await governed.ainvoke(...).

Tool input extraction

The adapter captures the input passed to each tool and records it in the payload:
  • dict inputs pass through unchanged
  • str inputs are wrapped as {"tool_input": "..."}
  • Pydantic model inputs call .model_dump() (v2) or .dict() (v1)
  • Other types fall back to str(input) (truncated)
Sensitive fields are sanitized using patterns configured in proofrail.init(). See Configuration Reference for both sensitive_field_patterns (key-name matching) and sensitive_value_patterns (value-prefix matching).

Edge cases

ReAct-style agents. Agents using the ReAct pattern (think → act → observe loops) work normally. Each “act” step’s tool call is a separate event. Multiple tool calls in parallel. If your agent uses parallel tool calling (modern OpenAI/Anthropic models), each parallel call is recorded as a separate event. Policy evaluation happens per-call; one denial doesn’t automatically halt others already in flight. Tools that themselves call agents. Nested agent calls (one agent’s tool is another agent’s executor) work, but currently flatten into the same chain. There’s no automatic parent-child relationship in the audit trail. Streaming responses. astream works but the streaming output isn’t customized for governance. Events are recorded as they happen; the stream itself is unchanged from the underlying executor. Custom runnables. Any Runnable that fires standard LangChain callbacks works with govern(), not just AgentExecutor. Custom chains, sequential pipelines, and Runnable graphs are all supported as long as they emit on_tool_start / on_tool_end callbacks. Tool wrapping vs. agent wrapping. govern() wraps the executor, not individual tools. If you want to govern a single tool outside an agent context, instrument it manually with proofrail.Chain.record_agent_action instead.

Where to go next

LangGraph adapter

For compiled-graph workflows with node-level granularity.

MCP adapter

Govern MCP server tool calls at the protocol level.

Policies

What decisions get applied to your tool calls.

SDK reference

The complete govern() signature and options.