Framework Guide

EU AI Act Compliance for LangGraph Agents

LangGraph runs your tools through a ToolNode. Fuze instruments those tools — not the graph or the state — so your StateGraph wiring is unchanged. fuze_tools() batch-wraps a tool list and emits a hash-chained audit record per tool call.

Your role under the EU AI Act

You are the Deployer of any AI system you build with LangGraph, and you may also be the Provider if you place that system on the EU market. Provider-side obligations for the underlying model lie with the model vendor (OpenAI, Anthropic, etc.). The system you assemble is yours; see deployer vs provider.

Fuze is a component supplier under Article 25(4). Fuze tooling does not certify your AI system — see Terms §6.

1. Install

pip install fuze-ai langgraph

The LangGraph adapter ships with the Python fuze-ai package (TypeScript LangGraph users wrap with the generic guard() instead — see the OpenAI Agents guide for the TS pattern).

2. Wrap your tools

from fuze_ai.adapters.langgraph import fuze_tools
from langgraph.prebuilt import ToolNode

# Your existing tools — unchanged.
tools = [search_tool, calculator_tool, email_tool]

# Batch-wrap with Fuze. Names, descriptions, schemas preserved.
guarded = fuze_tools(
    tools,
    config={
        "max_iterations": 25,
        "resource_limits": {
            "max_tokens_per_run": 200_000,
            "max_steps": 25,
            "max_wall_clock_ms": 60_000,
        },
    },
)

# Hand to ToolNode. Your graph wiring is unchanged.
tool_node = ToolNode(guarded)

fuze_tools() wraps each tool's _run method with @guard, preserving the tool name, description, and input/output schemas LangGraph relies on.

3. Mark side effects and per-tool overrides

# Mark irreversible tools as side effects, with compensation callables.
# If the run is killed mid-step, Fuze invokes the compensate function.
guarded = fuze_tools(
    tools,
    side_effects={
        "send_email":   recall_email,
        "create_record": delete_record,
    },
)

# Per-tool overrides — when you need different limits or behaviour
# for one tool without touching the rest.
guarded = fuze_tools(
    tools,
    per_tool={
        "search":     {"max_retries": 5},
        "send_email": {"side_effect": True, "max_retries": 1},
    },
)

4. Wire it into a graph

from langgraph.graph import StateGraph, MessagesState
from langgraph.prebuilt import ToolNode
from fuze_ai.adapters.langgraph import fuze_tools

tools = [search, calculator, send_email]
guarded = fuze_tools(tools, side_effects={"send_email": recall_email})

graph = StateGraph(MessagesState)
graph.add_node("tools", ToolNode(guarded))
# ... rest of graph setup

result = graph.compile().invoke(
    {"messages": [("user", "find recent revenue numbers")]},
)

5. Verify the audit trail

from fuze_ai import verify_chain, read_audit

records = read_audit()  # ~/.fuze/audit.jsonl
result = verify_chain(records)
print(result.ok, result.first_mismatch_index)

# Each record carries: step_id, run_id, step_number, tool_name,
# args_hash, tokens_in, tokens_out, latency_ms, error,
# prev_hash, hash, signature, sequence.

Records are HMAC-SHA256 hash-chained — tampering with one record breaks every record after it. Arguments are SHA-256 hashed (first 16 chars), so the audit trail doesn't leak PII even when the graph processes personal data.

What Fuze actually does, and what it doesn't

Fuze sees the tool-call boundary. Honest scope:

  • Tool-call audit log — every wrapped invocation recorded with step_id, run_id, tool_name, args_hash, tokens_in, tokens_out, latency_ms, error.
  • 3-layer loop detection — iteration cap, repeated tool-call dedup (sliding window on tool_name + args_hash), no-progress detection. Detects loops at the tool-call level. It does not parse LangGraph cycles— graph-state introspection is not implemented; the SDK doesn't see your edges or conditional routes.
  • Per-run resource limits — tokens, steps, wall-clock, shared across all tool calls within a run.
  • Side-effect compensation via the side_effects argument.
  • Kill switch — the daemon can halt before the next step; the wrapped tool refuses and logs a guard event.

Fuze does notunderstand LangGraph nodes, edges, conditional routing, checkpoints, or the StateGraph channels. Per-node and per-graph budgets do not exist — only per-run, where a "run" means "one continuous instrumented invocation chain." Fuze does not track cost in dollars, by design (see the budget docs); cross-run drift detection is not implemented.

Common high-risk LangGraph use cases

Use the risk classifier for a real determination — these are heuristics:

  • HR / recruitment workflows — Annex III category 4, high-risk.
  • Credit / underwriting graphs — Annex III category 5, high-risk.
  • Customer-service automation — limited risk; transparency obligations under Art. 50.
  • Research / data-extraction graphs — usually minimal risk; the Art. 6(3) research carve-out may apply.

Mapping to AI Act articles

  • Article 12 (record-keeping): the JSONL hash-chained log per tool call. Retention is your call.
  • Article 14 (human oversight): side_effects + compensate callables give a defensible kill-and-rollback path. Human approval before a high-impact tool runs is your responsibility — wrap the call site in your own check.
  • Article 15 (robustness): per-run resource limits and 3-layer loop detection. Note: cycle detection here is at the tool level, not at the graph-edge level.

When to consider Fuze Agent instead

If you're starting a new agent project and want compliance baked in — typed tools, explicit data classification, Ed25519-signed run-roots, replay-protected HITL approvals, EU-residency as a type — see Fuze Agent. Fuze Agent is TypeScript only; if your team is on Python, stay on LangGraph + fuze_tools().