> ## Documentation Index
> Fetch the complete documentation index at: https://docs.proofrail.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# LangChain

> Govern LangChain agents and tool executors with one wrap

# 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](/frameworks/langgraph) 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

```bash theme={null}
pip install "proofrail[langchain] @ git+https://github.com/TOAAiV/proofrail"
```

This installs `langchain` and `langchain-core` if not already present.

<Note>
  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`.
</Note>

## Quick start

```python theme={null}
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:

```python theme={null}
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`](/quickstart) directly and call `record_agent_action(agent_name="support-agent-v2", ...)` yourself for full control

## Configuring the chain

```python theme={null}
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:

```python theme={null}
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

```python theme={null}
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](/reference/configuration) 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

<CardGroup cols={2}>
  <Card title="LangGraph adapter" icon="diagram-project" href="/frameworks/langgraph">
    For compiled-graph workflows with node-level granularity.
  </Card>

  <Card title="MCP adapter" icon="plug" href="/frameworks/mcp">
    Govern MCP server tool calls at the protocol level.
  </Card>

  <Card title="Policies" icon="shield" href="/concepts/policies">
    What decisions get applied to your tool calls.
  </Card>

  <Card title="SDK reference" icon="code" href="/reference/sdk-api">
    The complete `govern()` signature and options.
  </Card>
</CardGroup>
