Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,9 @@ Brand new to the SDK? [`examples/wrap-your-agent`](examples/wrap-your-agent) is
no-key, two-minute version — a generic Python loop the governor caps at a loop
budget, the deterministic kill with nothing running but the daemon.

On LangChain? [`examples/langchain`](examples/langchain) wraps a LangChain loop
with the callback handler and caps it at a loop budget — also key-free.

## Design principles

- **Deterministic core in Go.** All enforcement (budgets, kill switches, gating, routing, retries, checkpointing) lives in compiled, statically-typed code — never in an LLM.
Expand Down
105 changes: 105 additions & 0 deletions examples/langchain/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# langchain — stop a runaway LangChain agent

Wrap a LangChain LLM loop with RiskKernel's callback handler and the
**deterministic governor caps it** — one governed step per model call, hard-stopped
at the loop budget. The halt propagates out of `llm.invoke()` and ends the chain.
The kill comes from RiskKernel, not from the script.

**No API key, no model call.** This uses LangChain's `FakeListLLM` so the loop
enforcement runs with nothing but `riskkernel serve`. (Add a real model — and the
dollar/token ceiling — with a few lines; see [below](#add-the-dollar--token-ceiling-real-model).)

## Run it in 60 seconds

```bash
# 1. start the daemon — no key needed for this demo
docker run --rm -p 7070:7070 ghcr.io/prashar32/riskkernel:latest

# 2. in another terminal, install langchain-core + the SDK, and run it
cd examples/langchain
pip install -r requirements.txt
python agent.py
```

## What you'll see

A real run (the run-id varies; the structure is the point):

```
â–¶ langchain loop budget = 6 (enforced by the Go governor)
run id: 31ab03d4-e7e4-47ad-bb74-6402999ca4fd

step 1 │ LLM call allowed by the governor
step 2 │ LLM call allowed by the governor
step 3 │ LLM call allowed by the governor
step 4 │ LLM call allowed by the governor
step 5 │ LLM call allowed by the governor
step 6 │ LLM call allowed by the governor

🛑 RiskKernel halted the LangChain run — reason: loop_budget_exceeded
── final ledger (enforced by the governor) ──
LLM calls (loops) : 6 (budget: 6)
run id : 31ab03d4-e7e4-47ad-bb74-6402999ca4fd
The agent would have looped forever; the governor capped it — and the
halt propagated out of llm.invoke(), ending the chain.
```

The 7th `llm.invoke()` is refused: the handler's `on_llm_start` ticks a governed
step, the daemon returns HTTP `402 loop_budget_exceeded`, and that surfaces as
`rk.BudgetExceeded` — which propagates out of LangChain and stops the loop.

## Wrapping your own LangChain agent

Two objects: the handler, and passing it as a callback.

```python
import riskkernel as rk
from riskkernel.adapters.langchain import RiskKernelCallbackHandler

rt = rk.Runtime() # http://localhost:7070
with rt.governed_run(budget=rt.budget(loops=50, seconds=600)) as run:
handler = RiskKernelCallbackHandler(run) # one governed step per LLM call
while not done:
llm.invoke(prompt, config={"callbacks": [handler]}) # raises rk.BudgetExceeded at the cap
... # your existing chain / tools
```

Works the same with `AgentExecutor`, `Runnable` chains, or LangGraph — anywhere you
can pass `callbacks`. The handler also gates tools on human approval when you
construct it with `RiskKernelCallbackHandler(run, gate_tools=True)`.

> The handler sets `raise_error = True` so the budget halt actually propagates —
> LangChain otherwise swallows exceptions raised inside a callback and the chain
> would keep spending past budget.

## Add the dollar / token ceiling (real model)

The handler caps **loops and time**. To also cap **dollars and tokens**, route the
model through the run's proxy so every call is priced from real provider usage and
the dollar budget halts the run:

```python
from langchain_openai import ChatOpenAI # speaks the OpenAI API to the proxy

cfg = run.proxy_config()
llm = ChatOpenAI(
base_url=cfg["base_url"], # http://localhost:7070/v1
default_headers=cfg["headers"], # groups calls into this governed run
api_key="sk-unused", # the real provider key lives in the daemon
model="claude-sonnet-4-5",
)
# budget=rt.budget(loops=50, dollars=1.00, seconds=600)
# the dollar ceiling trips at the proxy with HTTP 402; the loop/time ceiling
# trips in the handler. Start the daemon with your ANTHROPIC_API_KEY.
```

For the real-model dollar kill end to end — the cost ledger climbing each step
until the governor stops it — see [`examples/codebase-qa`](../codebase-qa).

## Tuning for a recording

- `LOOP_BUDGET` (default `6`) — lower it for a faster kill, raise it for more
steps before the halt.

Nothing about the kill is faked: it's the daemon returning HTTP `402` before the
over-budget call, surfaced through LangChain's own callback path.
98 changes: 98 additions & 0 deletions examples/langchain/agent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
#!/usr/bin/env python3
"""langchain — stop a runaway LangChain agent at its budget.

Wraps a LangChain LLM loop with RiskKernel's callback handler: one governed step
per model call. A deliberately runaway loop is hard-stopped by the deterministic
governor at its loop budget — and the halt propagates out of ``llm.invoke()``,
ending the chain. The kill comes from RiskKernel, not from this script.

Key-free: this uses LangChain's ``FakeListLLM`` so the loop enforcement runs with
nothing but ``riskkernel serve`` — no model call, no spend. To add the dollar /
token ceiling with a *real* model, route the LLM through the run's proxy; the
README shows the few extra lines.

Run it:
riskkernel serve # in another terminal — no key needed for this demo
pip install -r requirements.txt
python agent.py

The integration is two objects: ``RiskKernelCallbackHandler(run)`` and passing it
as a LangChain callback. The handler ticks one governed step per LLM call, so the
governor caps the loop the same way it would cap a real agent.
"""

from __future__ import annotations

import logging
import os

import riskkernel as rk
from riskkernel.adapters.langchain import RiskKernelCallbackHandler

# LangChain logs any exception raised inside a callback as an "error" — even the
# BudgetExceeded we raise on purpose to stop the run. Quiet just that logger so the
# demo output stays clean; the halt is intentional and handled below.
logging.getLogger("langchain_core.callbacks.manager").setLevel(logging.CRITICAL)

try:
from langchain_core.language_models.fake import FakeListLLM
except ImportError:
raise SystemExit(
"this example needs langchain-core — install it with:\n"
" pip install -r requirements.txt"
)

# ─────────────────────────────────────────────────────────────────────────────
# Knobs. The kill is never faked: the governor (in the daemon) enforces the loop
# budget before each LLM call and the handler propagates the halt into LangChain.
# ─────────────────────────────────────────────────────────────────────────────
DAEMON_URL = os.environ.get("RISKKERNEL_BASE_URL", "http://localhost:7070")
LOOP_BUDGET = 6 # max LLM calls (loop iterations) the governor allows


def main() -> int:
rt = rk.Runtime(base_url=DAEMON_URL)
print(f"â–¶ langchain loop budget = {LOOP_BUDGET} (enforced by the Go governor)")

# A stand-in model so the demo needs no key. Swap FakeListLLM for
# ChatAnthropic / ChatOpenAI pointed at run.proxy_config() to add the
# dollar/token ceiling on real calls — see the README.
llm = FakeListLLM(responses=["…thinking; I'll keep going…"] * 1000)

try:
with rt.governed_run(name="langchain-demo",
budget=rt.budget(loops=LOOP_BUDGET)) as run:
print(f" run id: {run.id}\n")
handler = RiskKernelCallbackHandler(run)
step = 0
while True: # a deliberately runaway agent loop
try:
llm.invoke(f"step {step + 1}: decide the next action",
config={"callbacks": [handler]})
except rk.BudgetExceeded as halt:
_report_halt(run, halt)
return 0
step += 1
print(f" step {step:>2} │ LLM call allowed by the governor")
except rk.APIError as e:
if e.code == "connection_error":
print(f"\n✗ can't reach the daemon at {DAEMON_URL} — start it first:")
print(" riskkernel serve")
print(" # or: docker run --rm -p 7070:7070 ghcr.io/prashar32/riskkernel:latest")
return 1
raise


def _report_halt(run: rk.Run, halt: rk.BudgetExceeded) -> None:
"""Print the final, governor-enforced ledger for the halted LangChain run."""
usage = run.status().get("usage", {})
print(f"\n🛑 RiskKernel halted the LangChain run — reason: {halt.reason}")
print(" ── final ledger (enforced by the governor) ──")
print(f" LLM calls (loops) : {usage.get('loops', 0):>4} (budget: {LOOP_BUDGET})")
print(f" run id : {run.id}")
print(" The agent would have looped forever; the governor capped it — and the")
print(" halt propagated out of llm.invoke(), ending the chain.")


if __name__ == "__main__":
raise SystemExit(main())
12 changes: 12 additions & 0 deletions examples/langchain/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# This LangChain example needs two things: langchain-core (the callback machinery
# the adapter hooks into) and the RiskKernel Python SDK. The SDK isn't on PyPI yet,
# so it's installed from source; its core is stdlib-only. The git+https reference
# works from ANY directory (a bare relative path would resolve against your CWD).
#
# Tested with langchain-core 1.4.0; the adapter also supports the older
# langchain.callbacks.base import path.
langchain-core>=0.2
riskkernel @ git+https://github.com/prashar32/riskkernel.git#subdirectory=sdks/python

# Working inside a clone and want your local SDK instead? From THIS directory:
# pip install ../../sdks/python
Loading