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
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,19 @@ surface is governed by [`COMPATIBILITY.md`](COMPATIBILITY.md).
## [Unreleased]

### Added
- **AutoGen adapter (Python SDK).** `from riskkernel.adapters.autogen import
GovernedChatCompletionClient` — wrap your AutoGen model client once and hand it to
your existing `AssistantAgent` (or team) to bind it to a governed run with no other
code change. One model request counts as one governed step, so the deterministic
loop/time budget halts a runaway agent; with `gate_tools=True` each tool call the
model requests (a `FunctionCall` in the result) routes through the human-approval
gate before the agent can run it. Targets the actively maintained v0.4+ line
(`autogen-agentchat` / `autogen-core` >= 0.4), not the legacy `pyautogen` 0.2 API;
`autogen` is lazily handled (the wrapper is duck-typed and imports nothing), so it
stays an optional dependency. A single agent run propagates the halt typed; a team
(`RoundRobinGroupChat`, …) re-raises it as a `RuntimeError`, so `governed_run_errors()`
(from the same module) restores the typed `BudgetExceeded`/`ApprovalDenied` around a
team call.
- **OTLP trace ingress.** RiskKernel can now act as an OTLP/HTTP trace endpoint
(`POST /v1/traces`), the consume side of the OpenTelemetry surface — point any
exporter at it (`OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:7070`) and the GenAI
Expand Down
112 changes: 112 additions & 0 deletions examples/autogen/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# autogen — stop a runaway AutoGen agent

Wrap an AutoGen model client with RiskKernel's `GovernedChatCompletionClient` and
the **deterministic governor caps the agent** — one governed step per model request,
hard-stopped at the loop budget. The halt propagates out of `agent.run()` and ends
the run. The kill comes from RiskKernel, not from the script.

Targets the actively maintained **AutoGen v0.4+** line (`autogen-agentchat` /
`autogen-core`), not the legacy `pyautogen` 0.2 API.

**No API key, no model call.** This uses a tiny local model-client stub that always
returns the same tool call (so the agent loops forever), 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 autogen + the SDK, and run it
cd examples/autogen
pip install -r requirements.txt
python agent.py
```

## What you'll see

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

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

🛑 RiskKernel halted the AutoGen run — reason: loop_budget_exceeded
── final ledger (enforced by the governor) ──
model calls (loops) : 7 (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 agent.run().
```

The 7th model request is refused: the wrapper's `create()` ticks a governed step,
the daemon returns HTTP `402 loop_budget_exceeded`, and that surfaces as
`rk.BudgetExceeded` — which propagates out of AutoGen and stops the agent.

## Wrapping your own AutoGen agent

One object: wrap the model client, then use your agent as-is.

```python
import riskkernel as rk
from riskkernel.adapters.autogen import GovernedChatCompletionClient
from autogen_agentchat.agents import AssistantAgent

rt = rk.Runtime() # http://localhost:7070
with rt.governed_run(budget=rt.budget(loops=50, seconds=600)) as run:
client = GovernedChatCompletionClient(model_client, run) # one step per model call
agent = AssistantAgent("assistant", model_client=client) # otherwise unchanged
await agent.run(task="...") # raises rk.BudgetExceeded at the cap
```

Gate side-effecting tools on human approval by constructing it with
`GovernedChatCompletionClient(model_client, run, gate_tools=True)` — each tool the
model requests routes through the approval gate before the agent can run it.

## Teams re-raise the halt as a RuntimeError

A single agent run propagates the typed `rk.BudgetExceeded` directly. But a **team**
(`RoundRobinGroupChat`, `SelectorGroupChat`, …) catches an agent's exception and
re-raises it to the caller as a plain `RuntimeError` (its container serializes the
error and the team's `run()` re-raises `RuntimeError(str(error))`). The run still
halts — it is **not** swallowed — but the type is lost. Wrap the team call in
`governed_run_errors()` to get the typed exception back:

```python
from riskkernel.adapters.autogen import governed_run_errors

with governed_run_errors():
await team.run(task="...") # re-raises rk.BudgetExceeded, not RuntimeError
```

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

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

```python
from autogen_ext.models.openai import OpenAIChatCompletionClient # speaks OpenAI to the proxy

cfg = run.proxy_config()
real = OpenAIChatCompletionClient(
model="claude-sonnet-4-5",
base_url=cfg["base_url"], # http://localhost:7070/v1
api_key="sk-unused", # the real provider key lives in the daemon
default_headers=cfg["headers"], # groups calls into this governed run
)
client = GovernedChatCompletionClient(real, run)
# 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 wrapper. Start the daemon with your ANTHROPIC_API_KEY.
```

## 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 model request, surfaced through AutoGen's own model-client path.
159 changes: 159 additions & 0 deletions examples/autogen/agent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
#!/usr/bin/env python3
"""autogen — stop a runaway AutoGen agent at its budget.

Wraps an AutoGen model client with RiskKernel's ``GovernedChatCompletionClient``:
one governed step per model request. A deliberately runaway agent — one whose model
always asks to call a tool, so it never finishes on its own — is hard-stopped by the
deterministic governor at its loop budget, and the halt propagates out of
``agent.run()``. The kill comes from RiskKernel, not from this script.

Key-free: the model client here is a tiny local stub that always returns the same
tool call, 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, build
your real client (e.g. ``OpenAIChatCompletionClient``) pointed at the run's proxy
and wrap that instead; 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 one object: ``GovernedChatCompletionClient(model_client, run)``.
Hand the wrapped client to your existing ``AssistantAgent`` and the governor caps
the loop the same way it would cap a real agent.
"""

from __future__ import annotations

import asyncio
import os

import riskkernel as rk
from riskkernel.adapters.autogen import GovernedChatCompletionClient

try:
from autogen_agentchat.agents import AssistantAgent
from autogen_core import CancellationToken, FunctionCall
from autogen_core.models import (
ChatCompletionClient,
CreateResult,
ModelInfo,
RequestUsage,
)
from autogen_core.tools import FunctionTool
except ImportError:
raise SystemExit(
"this example needs autogen-agentchat + autogen-core — install them with:\n"
" pip install -r requirements.txt"
)

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


def _noop(query: str) -> str:
"""A do-nothing tool the agent keeps calling."""
return "keep going"


class _LoopingClient(ChatCompletionClient):
"""A stand-in model client so the demo needs no key: it always returns the same
tool call, so the agent loops forever — until the governor caps it. Swap this for
a real ``OpenAIChatCompletionClient`` / ``AnthropicChatCompletionClient`` pointed
at ``run.proxy_config()`` to add the dollar/token ceiling (see the README)."""

def __init__(self) -> None:
self._usage = RequestUsage(prompt_tokens=0, completion_tokens=0)

async def create(self, messages, *, tools=[], tool_choice="auto",
json_output=None, extra_create_args={}, cancellation_token=None):
return CreateResult(
finish_reason="function_calls",
content=[FunctionCall(id="1", name="noop", arguments='{"query": "again"}')],
usage=RequestUsage(prompt_tokens=1, completion_tokens=1),
cached=False,
)

async def create_stream(self, *args, **kwargs): # pragma: no cover
raise NotImplementedError

async def close(self) -> None:
return None

def actual_usage(self):
return self._usage

def total_usage(self):
return self._usage

def count_tokens(self, messages, *, tools=[]):
return 0

def remaining_tokens(self, messages, *, tools=[]):
return 1000

@property
def capabilities(self):
return self.model_info

@property
def model_info(self):
return ModelInfo(vision=False, function_calling=True, json_output=False,
family="unknown", structured_output=False)


async def _run() -> int:
rt = rk.Runtime(base_url=DAEMON_URL)
print(f"▶ autogen loop budget = {LOOP_BUDGET} (enforced by the Go governor)")

tool = FunctionTool(_noop, description="a no-op tool the agent keeps calling")

try:
with rt.governed_run(name="autogen-demo",
budget=rt.budget(loops=LOOP_BUDGET)) as run:
print(f" run id: {run.id}\n")
# The one integration line: wrap the model client. Everything below is a
# plain AutoGen agent — no other change.
client = GovernedChatCompletionClient(_LoopingClient(), run)
agent = AssistantAgent(
"looper", model_client=client, tools=[tool],
reflect_on_tool_use=False, max_tool_iterations=1000,
)
try:
await agent.run(task="keep using the tool",
cancellation_token=CancellationToken())
except rk.BudgetExceeded as halt:
_report_halt(run, halt)
return 0
print(" (agent finished on its own before the budget — unexpected here)")
return 0
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 AutoGen run."""
usage = run.status().get("usage", {})
print(f"\n🛑 RiskKernel halted the AutoGen run — reason: {halt.reason}")
print(" ── final ledger (enforced by the governor) ──")
print(f" model 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 agent.run().")


def main() -> int:
return asyncio.run(_run())


if __name__ == "__main__":
raise SystemExit(main())
9 changes: 9 additions & 0 deletions examples/autogen/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# This AutoGen example needs autogen-agentchat + autogen-core (the v0.4+ line — the
# model-client protocol the adapter wraps) and the RiskKernel Python SDK (stdlib-only).
# Tested with autogen-agentchat / autogen-core 0.4+; the adapter supports >=0.4,<1.
autogen-agentchat>=0.4,<1
autogen-core>=0.4,<1
riskkernel

# Working inside a clone and want your local SDK instead? From THIS directory:
# pip install ../../sdks/python
10 changes: 10 additions & 0 deletions sdks/python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,18 @@ hooks = RiskKernelRunHooks(run, gate_tools=True)
# CrewAI — a step_callback (one governed step per agent step; halts the crew at budget)
from riskkernel.adapters.crewai import RiskKernelStepCallback
crew = Crew(agents=[...], tasks=[...], step_callback=RiskKernelStepCallback(run))

# AutoGen (v0.4+) — wrap the model client; one governed step per model call
from riskkernel.adapters.autogen import GovernedChatCompletionClient
client = GovernedChatCompletionClient(model_client, run) # drop-in for the real client
agent = AssistantAgent("assistant", model_client=client) # halts the agent at budget
```

> AutoGen halts the run either way, but a *team* (`RoundRobinGroupChat`, …) re-raises
> the halt as a `RuntimeError`; wrap the team call in
> `with governed_run_errors():` (from the same module) to get the typed
> `rk.BudgetExceeded` back. A single agent run propagates it typed already.

## Configuration

`Runtime(base_url=..., token=...)`, or the env vars `RISKKERNEL_BASE_URL` and
Expand Down
1 change: 1 addition & 0 deletions sdks/python/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ Source = "https://github.com/prashar32/riskkernel"
[project.optional-dependencies]
langchain = ["langchain-core"]
crewai = ["crewai>=0.80,<2"]
autogen = ["autogen-agentchat>=0.4,<1", "autogen-core>=0.4,<1"]
dev = ["pytest"]

# This project lives in a subdirectory (sdks/python) of the repo. Hatchling's
Expand Down
1 change: 1 addition & 0 deletions sdks/python/riskkernel/adapters/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@
- ``claude_agent`` — a PreToolUse hook for the Claude Agent SDK (approval gate).
- ``openai_agents`` — RunHooks for the OpenAI Agents SDK (steps + approval gate).
- ``crewai`` — a step_callback for CrewAI (steps + tool approval gate).
- ``autogen`` — a model-client wrapper for AutoGen v0.4+ (steps + tool gate).
"""
Loading