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 langgraphThe 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_effectsargument. - 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().