LangChain adapter
The LangChain adapter wraps either a legacy AgentExecutor or a compiled LangChain 1.x agent graph, such as the object returned by langchain.agents.create_agent.
flowchart TD
subgraph Host["LangChain Context"]
AE["AgentExecutor / Compiled Graph"]
T1["search (BaseTool)"]
T2["delete_records (BaseTool)"]
end
subgraph GATE["GATE Protection"]
G1["GATE Tool Guard"]
G2["GATE Tool Guard"]
MON["gateMonitor"]
end
AE -->|"calls ._run()"| G1
AE -->|"calls ._run()"| G2
G1 -->|"inspects"| MON
G2 -->|"inspects"| MON
MON -->|"ALLOW"| T1
MON -->|"BLOCK"| BLK["ToolExecutionBlocked Exception"]
style Host fill:#f0fbf7,stroke:#2eb85c,stroke-width:1.5px
style GATE fill:#fdfaff,stroke:#a855f7,stroke-width:2px
style BLK fill:#fde2e2,stroke:#d62728,color:#990000
style T1 fill:#e6f9ed,stroke:#2eb85c,color:#006622
Wrap an AgentExecutor
from gate import AgentProfile, gateConfig, wrap_agent
config = gateConfig(
agent_profile=AgentProfile(
agent_id="lc-agent",
role="assistant",
allowed_tools={"search"},
),
enabled_checks={"privilege", "dfa"},
)
executor = AgentExecutor(agent=agent, tools=[search, delete_records])
secure = wrap_agent(executor, "langchain", config)
try:
result = secure.invoke({"input": "Find the retention policy"})
finally:
secure.terminate()
The adapter reads .tools from the executor and installs a guard on every tool in that list. The executor and its agent keep their existing references, so no re-registration is needed.
Wrap a compiled agent graph
from langchain.agents import create_agent
graph = create_agent(model, tools=[search, delete_records])
secure = wrap_agent(graph, "langchain", config)
# If discovery cannot see the tools, name them explicitly:
secure = wrap_agent(graph, "langchain", config, tools=[search, delete_records])
Accepted graph type is langgraph.graph.state.CompiledStateGraph. Tools are discovered from ToolNode instances in the compiled graph using the same traversal as the LangGraph adapter.
Tool resolution order
- The
tools=argument, if provided. agent.tools, if it is a list or tuple.- Tools registered on
ToolNodes in the compiled graph. - For a compiled graph with no
ToolNode: an empty list, and the graph is wrapped with no guards. - Otherwise:
AdapterError.
Step 4 is the sharp edge. A compiled graph with tools hidden inside a nested graph that does not expose a ToolNode will be wrapped successfully and silently leave those tools unguarded. If you are not certain what discovery finds, pass tools= explicitly:
secure = wrap_agent(graph, "langchain", config, tools=[search, delete_records, nested_tool])
Check the actual guarded set by inspecting the tool objects after wrapping:
assert getattr(search, "_gate_enforcement", None) is secure.enforcement
State and identifiers
extract_state() and get_state() return the enforcement’s AgentState. get_agent_id() uses an agent_id attribute if the wrapped object has one; otherwise it generates langchain-agent-<id>. The generated identifier is not the agent_id in your profile. The profile identifier is used for privilege checks because the enforcement state is initialized from config.agent_profile.agent_id.
The adapter does not inject identifiers into the LangChain config. If your graph depends on configurable.agent_id, pass it yourself.
Invocation methods
| Method | Behavior |
|---|---|
invoke(input, **kwargs) |
Synchronous; runs the executor/graph under config.timeout_seconds |
ainvoke(input, **kwargs) |
Asynchronous; uses asyncio.wait_for |
get_state() |
Current AgentState |
terminate() |
Closes the monitor event loop |
The intent anchor for the semantic stage is str(input) when the input is not a string. For a message dictionary, that is a Python repr of the dictionary.
Async tools
langchain_native_async(tool) decides whether an async call should await the tool’s own _arun or run _run in a thread. StructuredTool._arun without an explicit coroutine delegates to _run; treating that as native would validate the same call twice. Tools with a real coroutine are awaited directly.
Errors
ToolExecutionBlocked is raised from _run or _arun. Whether AgentExecutor catches tool exceptions depends on its handle_tool_error setting; the guard’s exception reaches whatever handler the executor has. If the executor is configured to convert tool errors into observations, the model may see the refusal text; otherwise the exception propagates out of invoke.
Do not confuse this with a normal tool failure. ToolExecutionBlocked means no tool body ran. See Enforcement plumbing.
Limitations
- Tools added to the executor after wrapping are not guarded.
- A shared tool object cannot be guarded by two wrapped agents; the second wrap raises
AdapterError. - A compiled graph without a discoverable
ToolNodegets zero guards unlesstools=is passed. - The adapter does not mediate retriever calls, model calls, or callbacks.