Start free

LangChain

Gate the tool calls inside a LangChain agent by attaching HESO as a callback handler — every tool the agent invokes is captured, gated, signed, and audited. No tool definitions change; you add one callback.

A LangChain agent decides which tools to run and with what arguments. HESO sits at that boundary. You attach heso.HesoCallbackHandler as a callback, and from then on every tool the agent invokes is evaluated against your policy before it executes — allowed, blocked, redacted, or routed to a human — and recorded as a signed Action Receipt.

The callback handler

HesoCallbackHandler is a LangChain callback that gates tool calls. It is loaded on first use, so a plain import heso never requires LangChain to be installed — the handler loads only the moment you reference heso.HesoCallbackHandler. So the base SDK import pulls in nothing extra, and LangChain users still get a one-line integration.

The handler hooks the tool boundary. When the agent is about to run a tool, HESO:

  • captures the call as a tool_call action, with the tool name and its arguments as fields;
  • evaluates it against your policy and resolves a decision path;
  • signs the result into a receipt and appends it to the tamper-evident audit chain;
  • lets the tool run, or refuses it, according to that decision.

Wire it up

Call heso.init() once at startup, then pass the handler in the callbacks= list your agent or executor already accepts. Which LangChain classes you use depends on your setup; the hook is always the standard callbacks= argument.

agent.py
import hesofrom langchain.agents import AgentExecutor  # your agent setup, unchanged heso.init() # Attach HESO as a callback. Every tool the agent invokes is gated# at the boundary — captured, evaluated against policy, and signed.executor = AgentExecutor(    agent=agent,    tools=tools,    callbacks=[heso.HesoCallbackHandler()],) executor.invoke({"input": "refund order 4821"})

To label the actions a run produces, wrap the call in heso.step(...). Everything gated inside the block carries that workflow label. The label shows up on each receipt, so a run is easy to find later.

agent.py
import heso with heso.step(workflow="support-refunds"):    executor.invoke({"input": "refund order 4821"})

What gets captured

Each tool the agent invokes becomes one tool_call action. The tool name lands in tool_name, and the call arguments become the action’s fields. Your policy then decides the outcome along one of the four decision paths:

  • allow — the tool runs and a receipt is signed.
  • block — the tool is refused; it never executes.
  • redact — named fields are redacted before the receipt is signed, so secrets stay off our servers.
  • require_approval — the call is routed to a human approver before it can proceed.

See Actions & verbs for the full shape of what each tool call records, and Policy & decisions for how first-match-wins rules pick the path.

Combine with heso.wrap

The callback gates the agent’s tools. To gate the model calls the agent makes as well, wrap the underlying LLM client with heso.wrap(...). The two work side by side: the wrapped client gates each completion as an llm_call, and the callback gates each tool_call.

agent.py
import hesofrom langchain_openai import ChatOpenAI heso.init() # Gate the model calls themselves…llm = heso.wrap(ChatOpenAI(model="gpt-4o")) # …and the tools the agent runs, via the callback.executor = AgentExecutor(    agent=agent,    tools=tools,    callbacks=[heso.HesoCallbackHandler()],)

For the details of wrapping an LLM client — including the OpenAI and Anthropic clients directly — see OpenAI & Anthropic.

Blocked tools

When policy blocks a tool, the handler raises BlockedError instead of letting the tool run. The agent cannot execute the refused call. A receipt with decision_path set to block is still signed and appended to the audit chain, so the refusal itself is recorded as evidence.

agent.py
import hesofrom heso import BlockedError try:    executor.invoke({"input": "delete every customer record"})except BlockedError as e:    # Policy refused the tool at the boundary. The tool never ran;    # a receipt with decision_path="block" was still signed and audited.    print("tool refused:", e)
HESO gates the tool boundary, not the model output

The callback gates what the agent does — the tools it invokes and the arguments it passes. It does not, and cannot, gate the text the model writes. A receipt proves the operator authorized this tool call under a known policy; it does not prove the model’s reasoning was sound or its answer was true. If the agent narrates a refund it was blocked from issuing, the receipt records that the tool was refused — not that the narration is accurate.

Next steps