Skip to main content
langchain-neuraltrust makes a LangChain 1.x agent a TrustGuard collector. It is an AgentMiddleware, so it hooks the agent loop itself rather than the HTTP path: prompts are evaluated before the model is called, responses after, and tool calls can be gated before they execute. Nothing about your model provider changes. This suits an agent you own the code of. If you want coverage without touching application code, put a gateway in front instead — that covers every client at once, but cannot see individual tool calls.

Before you start

Keep the policy in Report mode for the first rollout. Report downgrades every rule to report, so findings appear in Activity without breaking traffic. Switch to Enforce once the finding volume looks right — see Policies.

1. Install

2. Add the middleware

Credentials come from the environment, so nothing secret belongs in the constructor:
Pass your tool list as payload_tools, never tools. AgentMiddleware.tools is declared by LangChain as “additional tools registered by the middleware”, so setting it does not describe your agent to TrustGuard — it registers those tools with the agent, changing its tool set. payload_tools is the correct name: it only adds the tool schemas to the input-stage evaluate payload, which is what lets tool-abuse rules see what the agent is allowed to do.
Every setting falls back to an environment variable: Leaving session_id unset is usually right: it picks up the LangGraph thread_id, which is already the conversation identity, so Activity groups turns the way your app does.

3. Choose what gets evaluated

Four independent flags, each mapping to a hook in the agent loop: check_tool_results is skipped when check_input is also on, because the conversation payload already contains those tool messages — you would be paying twice to evaluate the same text. check_tool_calls is the one worth adding deliberately. It is the only hook that sees a tool call before it executes, so it is what stops a hijacked agent from actually running the destructive call rather than reporting on it afterwards. It costs one round trip per tool call. Both sync (invoke) and async (ainvoke) paths are implemented for every hook.

4. Know how each verdict lands

The middleware never changes the meaning of a verdict, only how it is applied to agent state: Preserving the id is what makes transform usable in an agent: a redacted message must occupy the same slot in the thread, or the model sees both the original and the redaction. exit_behavior decides what a block does:
replace is not the gentler option. On the input stage the evaluated span is the whole conversation, so an input block overwrites every earlier turn as well as the current one — where end drops only the current turn and leaves the history intact. With a checkpointer that overwrite is persisted. On the output stage the span is the last AI message, and on tool results it is the originating AI message plus its ToolMessages, which is what most people picture.
SystemMessages are never removed by any of these. Under end and replace the middleware also keeps the thread structurally valid — a blocked AIMessage has its tool_calls cleared, and a blocked ToolMessage is converted so it cannot be left orphaning a tool response. An agent thread with a tool_call that never got an answer will fail on the next model call, which would turn a security block into a crash. On the tool path a block returns a ToolMessage(status="error") and the tool is never called.

5. Choose fail-open or fail-closed

unreachable_fallback decides what happens when TrustGuard genuinely cannot be reached, and it is deliberately narrow. It applies only to plain connect errors, timeouts, HTTP 502/504, and HTTP 429 after retries are exhausted. Those are retried with backoff, honouring Retry-After, before the fallback applies. Everything else fails closed even when you asked for fail_open:
  • HTTP 401/403, and 503 entitlement failures
  • any other 4xx/5xx
  • a non-JSON 200, or an unknown verdict
  • a transformed_payload that cannot be applied safely
  • TLS failures — an expired or untrusted certificate is a connect error, but it is treated as a configuration fault rather than an outage, so fail_open does not cover it. A corporate MITM proxy without a trusted CA will therefore block every turn even with fail_open set.
That last one matters more than it looks. If a transform came back misaligned — a different message count, a changed role, a rewritten tool name or id, an injected non-text content block — the middleware refuses it and fails closed rather than applying a partial redaction. Silently forwarding a half-masked prompt while the console shows a successful transform is the worst available outcome, so it is not an option.

6. Verify

With a jailbreak rule in Enforce mode the final message is the blocked AIMessage, the model is never called, and on_violation prints the trace_id. That trace_id is the same identifier the finding carries in Activity, so use it to reconcile a run with what the console shows. on_violation is synchronous and fires from both invoke and ainvoke. Exceptions from it propagate untouched — it is a hook for your own alerting, not a place where a bug should look like a TrustGuard failure. Call close() after invoke, or await aclose() after ainvoke, when the middleware owns its HTTP clients.

Streaming responses

after_model runs on the assembled AIMessage, once the model call has finished.
With stream=True, tokens have already reached the client by then. An output-side block or transform is detection after the fact, not prevention. Input-side and tool-call enforcement are unaffected and still happen before anything runs.
For preventive enforcement on the response, either disable streaming on the routes that need it, or buffer the stream until a verdict is available — at the cost of the time-to-first-token that streaming exists to provide.

Limits to keep in mind

  • Only this agent is inspected. Another service calling the provider directly bypasses TrustGuard, so use a gateway when the guarantee has to be network-wide.
  • Each guarded stage costs a round trip, and the hooks run again on every pass through the agent loop. With check_input, check_output and check_tool_calls all on, a turn that calls two tools in parallel and then answers is six evaluate calls, not four: input and output on each of the two model passes, plus one per tool call. Sequential tool calls cost more. Keep timeout tight enough that a stalled call cannot hold a run open.
  • Detectors evaluate text and tool calls. Non-text content parts are not stripped from the payload — a message whose content is a list of blocks is forwarded to /v1/evaluate as-is, so image data an agent sees is transmitted. What is limited is the write path: a transform that tries to alter a non-text part, or to swap a block’s type, is refused and fails closed.
Full contract for the endpoint behind all of this: Evaluate API. Source and issues: NeuralTrust/langchain-neuraltrust.