Govern LangGraph Agent Workflows with Sidecar Adapter
Attach Agentic Sidecar's native LangGraph adapter to intercept graph tool invocations and enforce intent constraints in govern mode without changing core agent logic.
| For | LangGraph agent developers adding real-time decision supervision |
| Difficulty | Intermediate |
| Time | ~15 minutes |
| Capabilities | LangGraph Adapter, Intent Guardian |
| Status | Stable |
The problem
LangGraph agents organize complex LLM applications into state graphs where nodes execute tool calls autonomously. While LangGraph provides workflow control, it does not evaluate whether tool parameters chosen by the LLM comply with runtime user authorization limits, financial risk boundaries, or organization policies.
Attempting to embed hardcoded validation logic inside every LangGraph tool or node couples governance rules to agent orchestration. Furthermore, modifying graph nodes directly prevents reusing tools across different security roles or operating modes.
Architecture
Keep the graph. Govern the tool boundary.
governed_tools = attach(sidecar, [issue_refund])↓create_react_agent(model, tools=governed_tools)Core graph logic stays unchanged. The adapter wraps the callable tools.refund_customer · requester user123 · order A100
issue_refund.amount ≤ maximum_refund ($500)Policy: default allow · Risk: LOW · Sidecar failure: fail_closed- 01 / GRAPH AGENT NODE✓◈
A model proposes a call.
AIMessage.tool_callsissue_refund("A100", 850)
TOOL CALL EMITTED - 02 / SIDECAR ADAPTER✓⇄
Intercept before execution.
governed_toolsThe wrapped callable checks the proposed arguments before invoking the original tool.
BOUNDARY INTERCEPTED - 03 / INTENT GUARDIAN×◇
BLOCK
$850 > $500The amount violates the user-authorized maximum. Risk remains LOW.
INTENT CONSTRAINT VIOLATED - 04 / ORIGINAL TOOL–⌁
Tool not called.
issue_refund()The block prevents the original callable from executing.
NOT DISPATCHED - 05 / GRAPH OUTCOME✓↩
Exception reaches the caller.
SidecarBlockedErrorThe surrounding try/except catches the blocked invocation. No successful final reply is produced.
agent.invoke RAISES - 06 / DECISION PROVENANCE✓▤
The verdict stays inspectable.
sidecar.decisionsBLOCK · issue_refund · risk LOW · Intent Guardian constraint violation.
ILLUSTRATIVE DECISION LOG
Blocked at the tool boundary.
The $850 attempt raises SidecarBlockedError before the real tool runs. The graph’s host handles the exception.
Inspect the illustrative graph result
{"simulation":true,"amount":850,"decision":"BLOCK","risk":"LOW","reason":"maximum_refund constraint violated","tool_executed":false,"exception":"SidecarBlockedError","final_message":null}Prerequisites
- Python 3.10+ (Python 3.12 recommended)
agentic-sidecarpackage withlanggraphextra dependencies installed:- Runnable script:
examples/langgraph_intent_guardian_govern_mode.py
Walkthrough
1. Install Required Extra Dependencies
Install agentic-sidecar with LangGraph adapter support:
2. Define LangGraph Tool and Scripted Model
Define the target tool issue_refund and import LangGraph components:
from typing import Any
from langchain_core.language_models.chat_models import BaseChatModel
from langchain_core.messages import AIMessage, BaseMessage
from langchain_core.outputs import ChatGeneration, ChatResult
from langgraph.prebuilt import create_react_agent
def issue_refund(order_id: str, amount: float) -> str:
"""Issue a refund for an order."""
return f"refunded ${amount:.2f} for order {order_id}"
class ScriptedToolCallingModel(BaseChatModel):
"""Replays a fixed sequence of `AIMessage`s -- see
`langgraph_refund_observe_mode.py` for why this exists.
"""
responses: list[AIMessage]
step: int = 0
def bind_tools(self, tools: Any, **kwargs: Any) -> "ScriptedToolCallingModel":
return self
def _generate(
self,
messages: list[BaseMessage],
stop: list[str] | None = None,
run_manager: Any = None,
**kwargs: Any,
) -> ChatResult:
message = self.responses[self.step]
self.step += 1
return ChatResult(generations=[ChatGeneration(message=message)])
@property
def _llm_type(self) -> str:
return "scripted"
def refund_request(order_id: str, amount: float) -> AIMessage:
return AIMessage(
content="",
tool_calls=[
{
"name": "issue_refund",
"args": {"order_id": order_id, "amount": amount},
"id": "call_1",
}
],
)
3. Build Intent Envelope and Constraint Bindings
Create an IntentEnvelope authorizing refunds up to $500.00 and bind the constraint to the issue_refund tool's amount argument:
from agentic_sidecar import Sidecar, SidecarBlockedError
from agentic_sidecar.adapters.langgraph import attach
from agentic_sidecar.intent import ConstraintBinding, IntentEnvelope, IntentGuardian, Requester
# Define authorized intent envelope
envelope = IntentEnvelope(
goal="refund_customer",
requested_by=Requester(type="human", id="user123"),
constraints={"maximum_refund": 500.0},
)
# Bind constraint to tool argument
binding = ConstraintBinding(
constraint="maximum_refund",
tool="issue_refund",
arg_name="amount",
op="lte",
)
guardian = IntentGuardian(envelope, [binding])
# Initialize Sidecar runtime in GOVERN mode
sidecar = Sidecar(
on_sidecar_failure="fail_closed",
roles=["policy", "risk", "intent_guardian"],
intent=guardian,
mode="govern",
)
4. Attach Adapter to LangGraph Tools
Wrap the tool array using attach(sidecar, tools) before instantiating the LangGraph agent:
# Wrap tools with Agentic Sidecar governance adapter
governed_tools = attach(sidecar, [issue_refund])
# Pass governed tools directly into LangGraph create_react_agent
model = ScriptedToolCallingModel(responses=[refund_request("A100", 850.0)])
agent = create_react_agent(model, tools=governed_tools)
5. Execute Graph and Enforce Governance
Invoke the agent with an out-of-bounds request ($850.00) followed by a valid request ($120.00):
# Attempt 1: $850.00 refund (Exceeds $500.00 limit) -> Raises SidecarBlockedError
try:
agent.invoke({"messages": [("user", "Refund order A100 for $850.")]})
except SidecarBlockedError as exc:
print(f"Blocked before tool ran: {exc}")
# Attempt 2: $120.00 refund (Within limit) -> Allowed
model = ScriptedToolCallingModel(
responses=[refund_request("A100", 120.0), AIMessage(content="Refund issued.")]
)
agent = create_react_agent(model, tools=governed_tools)
result = agent.invoke({"messages": [("user", "Refund order A100 for $120.")]})
print(f"Agent response: {result['messages'][-1].content}")
Expected output
Run the complete runnable example script:
Console Output:
--- Attempt 1: refund of $850 (exceeds the $500 authorization) ---
[GOVERN] BLOCK tool=issue_refund args={'order_id': 'A100', 'amount': 850.0} risk=LOW reason=Intent Guardian: 'amount'=850.0 on 'issue_refund' violates constraint 'maximum_refund' (must be lte 500)
Blocked before the real tool ran: Sidecar blocked 'issue_refund'({'order_id': 'A100', 'amount': 850.0}): Intent Guardian: 'amount'=850.0 on 'issue_refund' violates constraint 'maximum_refund' (must be lte 500)
--- Attempt 2: refund of $120 (within the $500 authorization) ---
Final agent message: Refund issued.
--- Sidecar decision log ---
BLOCK issue_refund({'order_id': 'A100', 'amount': 850.0}) -- Intent Guardian: 'amount'=850.0 on 'issue_refund' violates constraint 'maximum_refund' (must be lte 500)
ALLOW issue_refund({'order_id': 'A100', 'amount': 120.0}) -- Policy Advisor: No policy rule matched tool 'issue_refund'; default effect is 'allow'; Risk Evaluator: No risk rule matched tool 'issue_refund'; default risk is 'LOW'; Intent Guardian: No intent constraints violated for 'issue_refund'
Verify it worked
- Verify that
SidecarBlockedErroris thrown on Attempt 1 ($850.00 refund), preventing tool execution. - Confirm that Attempt 2 ($120.00 refund) completes successfully and returns
"Refund issued.". - Check the decision log output to confirm
BLOCKandALLOWstatuses recorded insidecar.decisions.
Clean up
To remove installed extras or deactivate virtual environment:
Variations and next steps
- Observe Mode Auditing: Run in
mode="observe"first to monitor LangGraph tool calls without blocking graph transitions. - Learn more in Integrations Documentation and Features.
Tested with
- agentic-sidecar 0.7.0, LangGraph 1.x (
>=1.0,<2), Python 3.12, Ubuntu 24.04, verified 2026-10-15