---
title: LangChain / LangGraph
description: Connect a LangChain or LangGraph agent to Redline — experiments, production monitoring and runtime protection. The complete document, written to be followed by a coding agent inside the repository.
---

You are connecting a repository whose agent is built on **LangChain** or
**LangGraph** to the Redline platform: experiments AND production monitoring.
Follow these steps exactly and do not modify the repository's existing agent
code — the whole integration is ONE new file plus ONE instrumentation package.

## 0. Confirm the shape

Grep for `langgraph.prebuilt`, `create_react_agent`, `langchain.agents`,
`create_tool_calling_agent`. If present, this is **Shape B** and this is the
page. LangChain is PATCHED by the SDK: the experiment's attached tools appear
in the agent by themselves. Its spans need one package (§8a).

## 1. Install and scaffold

At the repository root, inside its virtualenv:

```bash
pip install redlineai-sdk openinference-instrumentation-langchain
redline init
```

The second package is for monitoring; install it now, **before** the first
`redline dev`, so the first session already has a waterfall. `redline init`
writes `agents.py` — never `redline.py`, which shadows the SDK.

## 2. Find the agent's entry point

Locate the compiled graph or agent and how it is called: `.invoke(`,
`.stream(`, `.ainvoke(`, `.astream(`, with `{"messages": [...]}` and a
`config`. Read the signature and note whether it is sync or async. **Do NOT
modify it.**

## 3. Wire it into `agents.py`

```python title="agents.py"
import uuid
from redline import agent
import my_app.support_graph as subject                 # unchanged

def _run(task, ctx) -> str:
    config = {
        "configurable": {"thread_id": str(uuid.uuid4())},
        "recursion_limit": 250,        # the default of 25 stops a real build mid-way
    }
    final, buffered = "", []

    def flush():
        if buffered:
            ctx.thinking("\n".join(buffered)); buffered.clear()

    for event in subject.agent.stream(
        {"messages": [{"role": "user", "content": task.prompt}]},
        config, stream_mode="updates",
    ):
        for update in (event or {}).values():
            for message in (update or {}).get("messages", []) or []:
                text = getattr(message, "content", "") or ""
                calls = getattr(message, "tool_calls", []) or []
                if calls:
                    flush()                                  # 5c
                    for call in calls:
                        ctx.tool(call.get("name", "?"), call.get("args", {}))
                elif getattr(message, "type", "") == "tool":
                    ctx.tool_result(getattr(message, "name", "?"), text)
                elif text:
                    buffered.append(text); final = text
    flush()
    return final

@agent(id="device-support", name="Device Support",
       description="A support agent whose tools and instructions change as it moves warranty check → classification → resolution.")
def run(task, ctx):
    return _run(task, ctx)
```

- **Sync or async follows the graph.** If your graph's middleware implements
  only the sync `wrap_model_call`, LangChain refuses `astream()` with
  `NotImplementedError` — stream synchronously, as above, with a plain `def`.
  If the graph is async, use `async def run` and `astream` — and **never**
  `asyncio.run()` inside it.
- `recursion_limit: 250`. LangGraph defaults to 25 steps and a real task needs
  a few hundred; without it the run stops with `GraphRecursionError`.
- `id` is a kebab-case slug. `task.prompt` is the instructions. RETURN the
  final answer as a string.

## 4. Attached tools — do NOTHING

`redline dev` patches `langgraph.prebuilt.create_react_agent` and
`langchain.agents.create_tool_calling_agent` **before it imports your files**.
The order matters: `from langgraph.prebuilt import create_react_agent` binds
the function into your module, and a patch applied afterwards cannot reach that
binding — which is why the CLI patches first.

**Do not call `redline_tools`** — that connects everything twice. Verify by
counting the agent's tools. Attached SKILLS arrive inside `task.prompt`.

> **Import order is the whole trick**
>
> If your repository constructs the agent at import time in a module that
> `agents.py` imports, the patch has already landed and it works. If something
> else imports the graph *before* `redline dev` starts — a server entry point
> that runs on import — the agent is built unpatched. Run `redline dev` from a
> clean process; do not start it from inside the app.

## 5. Three failures that are nearly guaranteed if you skip them

**5a. Tell the agent its tools exist.** The prompt for a narrow job says "I
have no way to do that" while `machine_run` sits unused. Add one line to the
system prompt saying attached tools may be used when the task calls for them.

**5b. A tool that raises must not kill the run.** LangGraph's default
`ToolNode` handler **re-raises**, which turns "you have no app yet" into a dead
run. Build with `handle_tool_errors=True` (or your own handler) so an exception
becomes the observation text the agent reads and recovers from.

**5c. Do not drop the reasoning.** Map the stream onto `ctx` as in §3. Buffer
streamed prose and flush it as one block at each tool call — a transcript of
four hundred one-word entries is unreadable.

## 6. Connect and verify

The agent's own environment must load the way the repo normally loads it.
Then, in `.env` at the repo root or exported:

```bash
REDLINE_URL=https://tryredlineai.co
REDLINE_API_KEY=rl_…          # Agents page → Runner key
redline dev
```

`redline dev` reads that `.env` itself — every variable in it.

`redline dev` **must be running on this machine at all times**: experiments
execute inside it, the agent shows ONLINE only while it runs, asset snapshots
for Protect upload through it, policies and honeypots re-arm through it every
20 seconds, dev sessions stream to Monitor through it.

- `REDLINE_URL` is the address of the Redline platform — nothing in this
  repository hosts it.
- `REDLINE_API_KEY` is the runner key from the Agents page. **Do not commit it.**

On a server or VM:

```bash
nohup redline dev > ~/redline-dev.log 2>&1 &
disown
```

Outbound only, no port. Success: `connected — device-support registered and online`.

## 7. When something is wrong

- **"no agents found"** → `agents.py` exports no `@agent` function.
- **Registration rejected** → id collides; pick another slug.
- **The agent says it has no tools** → §5a, or the graph was built before
  `redline dev` patched (see §4 callout).
- **The run dies on the first tool error** → §5b.
- **`GraphRecursionError` / stops mid-build** → `recursion_limit` (§3).
- **`NotImplementedError` from `astream`** → sync-only middleware; stream
  synchronously (§3).
- **Session shows only User/Agent rows, no model calls** → §8a package missing
  or installed after `redline dev` started. Install and restart.
- **`ask_user` ENDS the run** — return the questions as the answer.

Everything else: [known issues](/integrations/known-issues).

## 8. Monitoring — the same agent, watched in production

Nothing extra to write for dev: while `redline dev` is up, every conversation
ALSO streams to **Monitor → Sessions**.

**8a. REQUIRED — `pip install openinference-instrumentation-langchain`.** The
SDK only LISTENS for OpenTelemetry spans; LangChain emits none until this
package is in the venv. `redline dev` finds and activates it by itself — but
only if it was installed **before** `redline dev` started. VERIFY after the
first run: model calls and a token count on the session. One line per turn and
nothing between means the package is missing or arrived late.

**For the DEPLOYED agent** — real users, no `redline dev`:

```python title="agents.py"
from redline import observe

@observe(agent="device-support", session_arg="conversation_id",
         user_arg="user_id", input_arg="text")
def handle_message(conversation_id: str, user_id: str, text: str) -> str:
    result = subject.agent.invoke(
        {"messages": [{"role": "user", "content": text}]},
        {"configurable": {"thread_id": conversation_id}, "recursion_limit": 250},
    )
    return result["messages"][-1].content
```

`thread_id = conversation_id` keeps a multi-turn conversation on one LangGraph
thread **and** one Redline session. Config is environment only; without
`REDLINE_API_KEY` the decorator is inert.

**8b. Your application has to CALL `handle_message`.** A decorator only sees
calls to the function it is on. The repository's existing message pipeline must
call it — one call-site change. Without it, production sessions never appear.

**8c. It must return, not yield.** `@observe` does not wrap async generators.
If the entry point streams, drain it and return the text.

What the platform does with a session: Monitor → Sessions with the waterfall;
violations matched per span and re-judged within minutes; intents mined when
the session closes. See [monitoring](/integrations/monitoring).

## 9. Runtime protection — what to expect once connected (nothing to write)

LangChain's model calls go through the OpenAI client (or litellm), which the
SDK patches, so policies, the guard classifier and honeypots configured in the
console (**Protect → the agent → Policies**) reach the running process within
~20 seconds. A denied tool call is stripped from the model's response before
`ToolNode` can execute it and replaced with
`[redline] The call to <tool> was denied by policy: <reason>`. The guard scores
every user message and tool result; honeypots are injected beside the agent's
own tools. Streaming passes through unjudged; everything fails OPEN.
