Skip to content

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

LANGGRAPH ADAPTER / GOVERN MODE

Keep the graph. Govern the tool boundary.

Browser-only illustration
Illustrative result · $850 request blocked
WIRE ONCE / BEFORE GRAPH CREATIONgoverned_tools = attach(sidecar, [issue_refund])create_react_agent(model, tools=governed_tools)Core graph logic stays unchanged. The adapter wraps the callable tools.
AUTHORIZED INTENT

refund_customer · requester user123 · order A100

issue_refund.amount ≤ maximum_refund ($500)Policy: default allow · Risk: LOW · Sidecar failure: fail_closed
  1. 01 / GRAPH AGENT NODE✓

    A model proposes a call.

    AIMessage.tool_calls

    issue_refund("A100", 850)

    TOOL CALL EMITTED
  2. 02 / SIDECAR ADAPTER✓

    Intercept before execution.

    governed_tools

    The wrapped callable checks the proposed arguments before invoking the original tool.

    BOUNDARY INTERCEPTED
  3. 03 / INTENT GUARDIAN×

    BLOCK

    $850 > $500

    The amount violates the user-authorized maximum. Risk remains LOW.

    INTENT CONSTRAINT VIOLATED
  4. 04 / ORIGINAL TOOL–

    Tool not called.

    issue_refund()

    The block prevents the original callable from executing.

    NOT DISPATCHED
  5. 05 / GRAPH OUTCOME✓

    Exception reaches the caller.

    SidecarBlockedError

    The surrounding try/except catches the blocked invocation. No successful final reply is produced.

    agent.invoke RAISES
  6. 06 / DECISION PROVENANCE✓

    The verdict stays inspectable.

    sidecar.decisions

    BLOCK · issue_refund · risk LOW · Intent Guardian constraint violation.

    ILLUSTRATIVE DECISION LOG
06 / DECISION PROVENANCE

Blocked at the tool boundary.

The $850 attempt raises SidecarBlockedError before the real tool runs. The graph’s host handles the exception.

0Simulated tool calls
$0Simulated funds issued
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}
Animated illustration of the scripted LangGraph example below: $850 is blocked by the intent constraint; $120 runs and returns “Refund issued.” Both have default LOW risk. Setup wraps the tools before graph creation; it does not modify graph logic. No model, LangGraph runtime or payment API is invoked by this animation.

Prerequisites

Walkthrough

1. Install Required Extra Dependencies

Install agentic-sidecar with LangGraph adapter support:

uv sync --extra langgraph
# Or with pip:
pip install "agentic-sidecar[langgraph]"

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:

uv run --extra langgraph python examples/langgraph_intent_guardian_govern_mode.py

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

  1. Verify that SidecarBlockedError is thrown on Attempt 1 ($850.00 refund), preventing tool execution.
  2. Confirm that Attempt 2 ($120.00 refund) completes successfully and returns "Refund issued.".
  3. Check the decision log output to confirm BLOCK and ALLOW statuses recorded in sidecar.decisions.

Clean up

To remove installed extras or deactivate virtual environment:

deactivate

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