Skip to content

Commit 13d4eae

Browse files
awesome-proclaude
andcommitted
feat: LangGraph adapter (v0.4)
Add the first framework adapter: guardloop.adapters.langgraph.guarded_graph returns a GuardLoop-compatible agent you pass to GuardLoop.run(...). LangGraph nodes call LangChain chat models, which do not flow through the ctx.openai / ctx.anthropic wrappers, so the adapter binds a synchronous LangChain BaseCallbackHandler (raise_error=True, run_inline=True) to the RunContext: it runs the pre-flight budget check before each LLM call, records actual usage afterward, and routes tool calls through the per-tool circuit breaker and the tool-call budget. Cost / token / time caps, breakers, and llm_call / tool_call OpenTelemetry spans all apply inside a LangGraph run; the verifier retry loop wraps the whole graph run, with feedback injected into a copy of the input state. - new guardloop.adapters subpackage; guardloop.adapters.langgraph exports guarded_graph and GuardLoopCallbackHandler (not re-exported from the top-level package, so `import guardloop` stays dependency-light) - guarded_graph(..., reserved_output_tokens=N) sets the pre-flight output-token reservation, since LangChain chat models often omit max_tokens - public RunContext.circuit_breakers accessor (used by the adapter) - new `langgraph` optional extra; langgraph + langchain-core in the dev group - tests/test_langgraph_adapter.py (16 tests) + tests/langchain_fakes.py - examples/langgraph_guarded.py (no-key demo) - .github/workflows/ci.yml: pytest + ruff + pyright on push/PR, Python 3.11-3.13 - bump to 0.4.0; CHANGELOG, README, and docs/{roadmap,design,project-overview, pypi-publishing}.md updated Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 8edac69 commit 13d4eae

16 files changed

Lines changed: 2138 additions & 56 deletions

.github/workflows/ci.yml

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
8+
permissions:
9+
contents: read
10+
11+
jobs:
12+
test:
13+
name: Test (Python ${{ matrix.python-version }})
14+
runs-on: ubuntu-latest
15+
strategy:
16+
fail-fast: false
17+
matrix:
18+
python-version: ["3.11", "3.12", "3.13"]
19+
20+
steps:
21+
- name: Check out repository
22+
uses: actions/checkout@v6
23+
24+
- name: Set up uv
25+
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
26+
with:
27+
python-version: ${{ matrix.python-version }}
28+
29+
- name: Install dependencies
30+
run: uv sync --all-extras --group dev
31+
32+
- name: Lint (ruff)
33+
run: |
34+
uv run ruff check .
35+
uv run ruff format --check .
36+
37+
- name: Type-check (pyright)
38+
run: uv run pyright
39+
40+
- name: Tests
41+
run: uv run pytest --cov=guardloop

CHANGELOG.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,38 @@ All notable changes to GuardLoop are documented here. The format is based on
55
follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html) (pre-1.0:
66
minor releases may include breaking changes).
77

8+
## [0.4.0] - 2026-05-11
9+
10+
### Added
11+
12+
- **LangGraph adapter (`guardloop.adapters.langgraph`).** `guarded_graph(graph)`
13+
returns a GuardLoop-compatible agent callable you pass to `GuardLoop.run(...)`;
14+
a `GuardLoopCallbackHandler` (a synchronous LangChain `BaseCallbackHandler`)
15+
bound to the `RunContext` runs the pre-flight budget check before each LLM call,
16+
records actual usage afterward, and routes tool calls through the per-tool
17+
circuit breaker and the tool-call budget — so cost / token / time caps, breakers,
18+
and `llm_call` / `tool_call` OpenTelemetry spans all apply *inside* a LangGraph
19+
run. The verifier retry loop wraps the whole graph run, with verifier feedback
20+
injected into a copy of the input state (`feedback_to_state` to customise).
21+
`guarded_graph(..., reserved_output_tokens=N)` sets the output-token reservation
22+
for the pre-flight check (default `1024`), since LangChain chat models often omit
23+
`max_tokens`. Behind the new `langgraph` optional extra
24+
(`pip install "guardloop[langgraph]"`).
25+
- `guardloop.adapters` subpackage; `guardloop.adapters.langgraph` exports
26+
`guarded_graph` and `GuardLoopCallbackHandler`. (Adapters are intentionally not
27+
re-exported from the top-level `guardloop` package, so `import guardloop` stays
28+
dependency-light.)
29+
- `RunContext.circuit_breakers` — public read-only access to the per-tool circuit
30+
breaker registry (used by adapters; also handy for inspecting breaker state).
31+
- No-key demo `examples/langgraph_guarded.py`.
32+
- `.github/workflows/ci.yml` — runs pytest + ruff + pyright on push / pull request
33+
across Python 3.11–3.13.
34+
35+
### Changed
36+
37+
- `pyproject.toml`: new `langgraph` optional-dependency extra; `langgraph` /
38+
`langchain-core` added to the dev dependency group; `langgraph` keyword.
39+
840
## [0.3.0] - 2026-05-10
941

1042
### Added
@@ -77,6 +109,7 @@ minor releases may include breaking changes).
77109
- No-key demo `examples/runaway_cost_prevention.py`; packaged and published to
78110
PyPI via GitHub Actions OIDC Trusted Publishing.
79111

112+
[0.4.0]: https://github.com/awesome-pro/guardloop/releases/tag/v0.4.0
80113
[0.3.0]: https://github.com/awesome-pro/guardloop/releases/tag/v0.3.0
81114
[0.2.0]: https://github.com/awesome-pro/guardloop/releases/tag/v0.2.0
82115
[0.1.0]: https://github.com/awesome-pro/guardloop/releases/tag/v0.1.0

README.md

Lines changed: 63 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@ loops can be stopped before they burn through money, flaky tools can be cut off
88
before an agent retries them into a bigger incident, and confidently-wrong
99
answers get a second pass.
1010

11-
The v0.3 focus is intentionally sharp: **runtime guardrails for async Python
12-
agents** — direct OpenAI and Anthropic wrappers, protected tool calls, per-tool
13-
circuit breakers, and a verify-fix-retry loop.
11+
The v0.4 focus: **runtime guardrails for async Python agents, including agents
12+
built with LangGraph** — direct OpenAI and Anthropic wrappers, protected tool
13+
calls, per-tool circuit breakers, a verify-fix-retry loop, and a LangGraph
14+
adapter that puts all of it *under* an existing graph without rewriting it.
1415

1516
```python
1617
from guardloop import (
@@ -65,15 +66,16 @@ clear trace. GuardLoop puts an explicit execution layer around that loop:
6566

6667
```mermaid
6768
flowchart LR
68-
U["User code"] --> R["GuardLoop"]
69+
LG["LangGraph graph"] -. "guarded_graph(...)" .-> U
70+
U["Your agent"] --> R["GuardLoop"]
6971
R --> B["BudgetController"]
7072
R --> CB["CircuitBreakerRegistry"]
7173
R --> V["VerifierChain"]
7274
R --> T["OpenTelemetry spans"]
7375
R --> C["RunContext"]
7476
C --> O["Wrapped OpenAI client"]
7577
C --> A["Wrapped Anthropic client"]
76-
C --> W["Wrapped tools"]
78+
C --> W["Wrapped tools / LangChain callbacks"]
7779
V -. "feedback on retry" .-> C
7880
```
7981

@@ -113,6 +115,44 @@ that fails every retry comes back as `success=False` with
113115
`terminated_reason="verification_failed"` but with `output` still populated;
114116
set `VerifierConfig(raise_on_failure=True)` for a hard stop.
115117

118+
## Framework Adapters
119+
120+
GuardLoop is a wrapper, not a framework — so it slots *under* the agent
121+
frameworks you already use. The first adapter is for **LangGraph** (behind the
122+
`langgraph` extra):
123+
124+
```bash
125+
pip install "guardloop[langgraph]"
126+
```
127+
128+
```python
129+
from langchain_core.messages import HumanMessage
130+
131+
from guardloop import GuardLoop, BudgetConfig
132+
from guardloop.adapters.langgraph import guarded_graph
133+
134+
runtime = GuardLoop(
135+
budget=BudgetConfig(cost_limit_usd="0.10", token_limit=10_000, tool_call_limit=20),
136+
verifiers=[...], # optional: the verifier retry loop wraps the whole graph run
137+
)
138+
139+
agent = guarded_graph(my_compiled_graph, input_key="messages")
140+
result = await runtime.run(agent, {"messages": [HumanMessage("research agent runtime safety")]})
141+
print(result.success, result.cost_usd, result.tokens_used, result.terminated_reason)
142+
```
143+
144+
`guarded_graph` returns a GuardLoop-compatible agent, so you keep calling
145+
`runtime.run(...)` as usual. A LangChain callback handler bound to the
146+
`RunContext` runs the pre-flight budget check before each LLM node, records usage
147+
afterward, and routes tool calls through the per-tool circuit breaker and the
148+
tool-call budget — so the cost / token / time caps, breakers, and `llm_call` /
149+
`tool_call` OpenTelemetry spans all apply *inside* the graph. A budget breach
150+
inside the graph terminates the run. On a verifier retry the feedback is injected
151+
into a copy of the input state (override `feedback_to_state` for non-standard
152+
state shapes). Because LangChain chat models often omit `max_tokens`,
153+
`guarded_graph(..., reserved_output_tokens=N)` sets the output-token reservation
154+
used by the pre-flight check (default `1024`).
155+
116156
## Project Guide
117157

118158
For a deeper walkthrough of what has been implemented, how the code is
@@ -170,6 +210,15 @@ malformed JSON). A verifier chain rejects it with feedback, the agent reads
170210
`ctx.retry_feedback` and self-corrects, and the run ends with
171211
`verification_passed: true` after three attempts.
172212

213+
```bash
214+
uv run python examples/langgraph_guarded.py
215+
```
216+
217+
This demo runs a small LangGraph graph (with an in-process fake chat model, so
218+
no API key) under `guarded_graph`. The first run succeeds with cost and tokens
219+
recorded; the second runs under a tiny token budget and is stopped before the
220+
model call.
221+
173222
## Live Provider Smoke Tests
174223

175224
```bash
@@ -192,7 +241,7 @@ uv run ruff format --check .
192241
uv run pyright
193242
```
194243

195-
## v0.3 Scope
244+
## v0.4 Scope
196245

197246
- Async Python runtime with `src/` package layout.
198247
- Hard caps for cost, tokens, time, and tool calls.
@@ -201,16 +250,22 @@ uv run pyright
201250
- Verify-fix-retry loop: sync or async output verifiers, fail-fast chains,
202251
built-in rule-based verifiers, feedback into `ctx.retry_feedback`, and an
203252
opt-in strict mode — all attempts share one budget and the run timeout.
253+
- LangGraph adapter (`guardloop.adapters.langgraph.guarded_graph`, behind the
254+
`langgraph` extra): budget caps, circuit breakers, and `llm_call` / `tool_call`
255+
OpenTelemetry spans applied inside a LangGraph run via a LangChain callback
256+
handler; the verifier loop wraps the whole graph run.
204257
- Direct wrappers for `AsyncOpenAI.responses.create` and
205258
`AsyncAnthropic.messages.create`.
206259
- OpenTelemetry spans for agent runs, LLM calls, tools, and verifiers.
207-
- Fake-client tests and demos that do not require API keys.
260+
- Fake-client tests and demos that do not require API keys; CI on push/PR
261+
(pytest + ruff + pyright, Python 3.11–3.13).
208262

209263
## Roadmap
210264

211265
- v0.2: per-tool circuit breakers. ✅
212266
- v0.3: verify-fix-retry loop. ✅
213-
- v0.4: LangGraph and OpenAI Agents SDK adapters.
267+
- v0.4: LangGraph adapter. ✅
268+
- v0.4.1: OpenAI Agents SDK adapter.
214269
- v0.5: Jaeger/Phoenix trace screenshots, demo video, and blog post.
215270
- v0.6: persistent breaker state, YAML/TOML policy, multi-model pricing, loop detection.
216271
- v1.0: stable API, changelog, docs site, release checklist.

docs/design.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,46 @@ agent produced an answer, it just isn't trusted. With
6666
`VerificationFailed` (same `terminated_reason`, `output=None`, attempt count and
6767
feedback in `metadata`).
6868

69+
## Framework Adapters
70+
71+
GuardLoop is not an agent framework, so it does not "support" frameworks — it
72+
*wraps* them. An adapter is just a thing that produces a GuardLoop-compatible
73+
`async def agent(ctx, ...)` callable; you still call `runtime.run(agent, ...)`. The
74+
adapters live in `guardloop.adapters`, each behind its own optional extra, and the
75+
core `GuardLoop` class never references a framework.
76+
77+
The LangGraph adapter (`guardloop.adapters.langgraph.guarded_graph`) is the first.
78+
LangGraph nodes call LangChain chat models, which do not go through GuardLoop's
79+
`ctx.openai` / `ctx.anthropic` wrappers, so the adapter hooks the cross-cutting seam
80+
LangChain *does* expose: a callback handler. `GuardLoopCallbackHandler` is a
81+
*synchronous* `BaseCallbackHandler` (LangChain only honours `raise_error` for sync
82+
handlers, and the handler does no I/O) with `raise_error = True` and
83+
`run_inline = True`. On `on_chat_model_start` / `on_llm_start` it estimates input
84+
tokens and runs `BudgetController.check_llm_call` (the pre-flight cost/token cap);
85+
on `on_llm_end` it records actual usage from the response's `usage_metadata` (or
86+
`llm_output["token_usage"]`); on `on_tool_start` / `on_tool_end` / `on_tool_error`
87+
it routes through `before_call` / `record_tool_call_started` / `record_success` /
88+
`record_failure`. Each LLM and tool call gets an `llm_call` / `tool_call` span that
89+
is a child of the active `agent_run` span. Guardrail exceptions raised inside the
90+
callbacks propagate out of `graph.ainvoke()` and are caught by `runtime.run`'s
91+
existing arms, so a budget breach inside the graph terminates the run.
92+
93+
Two consequences worth knowing. First, `check_llm_call` always needs an output-token
94+
reservation, but LangChain chat models frequently do not declare a `max_tokens`, so
95+
`guarded_graph(...)` exposes `reserved_output_tokens` (default 1024) as the fallback
96+
reservation. Second, tool-side enforcement is only as "hard" as the graph's own error
97+
handling: a `ToolNode` with its default `handle_tool_errors=True` will catch a
98+
`ToolCallLimitExceeded` / `CircuitBreakerOpen` raised by the callback and turn it
99+
into a `ToolMessage`, so the graph continues (the breaker still records the event and
100+
the LLM-side caps still terminate the run). Pass `handle_tool_errors=False` for hard
101+
tool-call enforcement. Streaming (`astream` / `astream_events`) is out of scope for
102+
v0.4.
103+
104+
The adapter needs `before_call` / `record_success` / `record_failure` on the per-tool
105+
breaker registry, so `RunContext.circuit_breakers` exposes it as a public read-only
106+
property (it persists on the `GuardLoop` instance across runs, like the breaker state
107+
itself).
108+
69109
## Telemetry
70110

71111
Provider wrappers emit OpenTelemetry spans through a small conventions module.

0 commit comments

Comments
 (0)