Skip to main content

Runtime budget, action, and audit authority for the OpenAI Agents SDK — enforce LLM cost limits, tool call caps, and audit trails before execution.

Project description

PyPI PyPI Downloads CI Coverage License

Cycles OpenAI Agents SDK Integration

Cycles governance for the OpenAI Agents SDK, powered by Cycles.

Prerequisites

Before you begin, make sure you have:

  1. Python 3.10+
  2. An OpenAI API key — required by the OpenAI Agents SDK to call LLMs
  3. A running Cycles server — see the deployment guide to set one up
  4. A Cycles API key — see API key management
  5. A tenant and budget — see tenant management and budget allocation

New to Cycles? The end-to-end tutorial walks through the full setup — from deploying the server to making your first budget-guarded API call — in about 10 minutes.

Why

The OpenAI Agents SDK gives you hooks and guardrails for content safety, but nothing for governance or action authority. Without Cycles governance:

  • A retry loop burns through $47 of API calls before anyone notices.
  • An agent with a send_email tool sends 200 emails in a single run because nothing limits it.
  • You can't give Tenant A a $10/day budget and Tenant B a $100/day budget — every tenant gets unlimited access.
  • There's no audit trail showing which agent called which tool, how many tokens it used, or what was consumed.

This plugin fixes all of that with one line:

result = await Runner.run(agent, input="...", hooks=CyclesRunHooks(tenant="acme"))

Every LLM call and every tool call in the entire agent run — including handoffs to sub-agents — automatically reserves budget before execution and commits actual usage after. If the budget is exhausted, the agent stops. No per-function decoration. No code changes to your tools.

What It Does

Problem How This Solves It
Runaway LLM spending Every LLM call reserves budget before running. DENY = agent stops.
Uncontrolled tool actions Tool estimate map assigns per-call estimates (send_email: 50, search: 0). Higher-estimate tools consume budget faster.
No per-tenant limits Pass tenant="acme" — Cycles enforces per-tenant budgets server-side.
No pre-run check cycles_budget_guardrail calls /v1/decide before the agent starts. Zero tokens consumed on DENY.
No audit trail Every reservation, commit, and handoff is recorded in the Cycles ledger.
Agent runs forever TTL heartbeat auto-extends reservations. If the agent dies, reservations expire and budget is released.

Installation

pip install runcycles-openai-agents

Setup

Set the following environment variables before running your agent:

# Required — OpenAI Agents SDK needs this to call LLMs
export OPENAI_API_KEY=sk-...

# Required — tells the plugin where your Cycles server is
export CYCLES_BASE_URL=http://localhost:7878
export CYCLES_API_KEY=cyc_live_...

Quick Start

from agents import Agent, Runner
from runcycles_openai_agents import CyclesRunHooks, cycles_budget_guardrail

# Pre-run budget check — agent never starts if budget exhausted
guardrail = cycles_budget_guardrail(tenant="acme-corp", estimate=5_000_000)

# Runtime governance — every tool/LLM call goes through Cycles
hooks = CyclesRunHooks(
    tenant="acme-corp",
    app="support-platform",
    tool_estimates={
        "send_email": 50,      # 50 RISK_POINTS per call
        "update_crm": 10,      # 10 RISK_POINTS per call
        "search_knowledge": 0, # zero estimate — no reservation
    },
)

agent = Agent(
    name="case-resolver",
    instructions="You resolve support cases.",
    input_guardrails=[guardrail],
)

result = await Runner.run(agent, input="...", hooks=hooks)

Hook lifecycle

The hooks plug into the SDK's native RunHooks interface and govern the entire agent run automatically:

Hook Cycles API Call Blocking Detail
on_tool_start create_reservation (tool estimate) Raises on DENY Budget reserved based on tool estimate map
on_tool_end commit_reservation No Actual amount committed
on_llm_start create_reservation (LLM estimate) Raises on DENY Budget reserved before each LLM call
on_llm_end commit_reservation (actual tokens) No Real token count from response.usage committed
on_handoff create_event (audit trail) No Handoff recorded in Cycles ledger

All raised exceptions from budget denial trigger BudgetExceededError. See Error Handling Patterns in Python for details.

Error handling

If Runner.run() raises, pending reservations stay locked until TTL expires. Call release_pending() to free them immediately:

hooks = CyclesRunHooks(tenant="acme-corp", app="support-platform")

try:
    result = await Runner.run(agent, input="...", hooks=hooks)
except Exception:
    await hooks.release_pending("agent_run_failed")
    raise

When budget is denied, the hooks raise BudgetExceededError:

from runcycles import BudgetExceededError

try:
    result = await Runner.run(agent, input="...", hooks=hooks)
except BudgetExceededError as e:
    print(f"Budget denied: {e}")
    # Agent stopped — no further tokens consumed

Guardrail (pre-run check)

cycles_budget_guardrail returns an InputGuardrail that calls /v1/decide before the agent starts. If the tenant is suspended or budget is exhausted, the guardrail trips and the agent never runs — zero tokens consumed:

from runcycles_openai_agents import cycles_budget_guardrail

guardrail = cycles_budget_guardrail(
    tenant="acme-corp",
    estimate=5_000_000,      # expected total run estimate
    unit=Unit.USD_MICROCENTS,
    fail_open=True,          # allow if Cycles server is down
)

agent = Agent(name="bot", input_guardrails=[guardrail])

Tool estimate mapping

Define an estimate policy once. New tools added to the agent get a default estimate automatically:

from runcycles_openai_agents import ToolEstimateMap, ToolEstimateConfig

hooks = CyclesRunHooks(
    tenant="acme-corp",
    tool_estimates=ToolEstimateMap(
        mapping={
            "send_email": 50,                       # 50 RISK_POINTS (default unit)
            "update_crm": ToolEstimateConfig(
                estimate=10,
                action_kind="tool.crm.update",
                unit=Unit.RISK_POINTS,              # explicit unit
            ),
            "search_knowledge": 0,                  # zero estimate — no reservation
        },
        default_estimate=1,                         # unmapped tools: 1 RISK_POINT
        default_unit=Unit.RISK_POINTS,              # unit for int shorthand values
    ),
)

Configuration

Explicit client

from runcycles import CyclesConfig, AsyncCyclesClient
from runcycles_openai_agents import CyclesRunHooks

config = CyclesConfig(base_url="http://localhost:7878", api_key="cyc_live_...")
client = AsyncCyclesClient(config)

hooks = CyclesRunHooks(client=client, tenant="acme-corp")

Fail-open / fail-closed

By default, if the Cycles server is unreachable the agent continues (fail_open=True). Set fail_open=False to enforce strict governance:

hooks = CyclesRunHooks(tenant="acme", fail_open=False)

All options

CyclesRunHooks(
    client=None,                # AsyncCyclesClient (or auto-created from config/env)
    config=None,                # CyclesConfig (creates client if no client given)
    tenant="acme-corp",         # Subject.tenant
    workspace="prod",           # Subject.workspace
    app="support-platform",     # Subject.app
    workflow="case-resolution", # Subject.workflow
    agent="case-resolver",      # Subject.agent (overridden by actual agent name)
    toolset=None,               # Subject.toolset (overridden by tool name)
    tool_estimates={"email": 50}, # dict or ToolEstimateMap (default unit: RISK_POINTS)
    default_tool_estimate=1,    # estimate for unmapped tools (in default unit)
    llm_estimate=500_000,       # per-LLM-call estimate (~$0.005 in USD_MICROCENTS)
    llm_unit=Unit.USD_MICROCENTS,
    fail_open=True,             # allow execution if Cycles is down
    ttl_ms=60_000,              # reservation TTL (heartbeat extends at half-interval)
    overage_policy=CommitOveragePolicy.ALLOW_IF_AVAILABLE,
    dry_run=False,              # shadow mode — no budget consumed
)

Features

  • Framework-native: Plugs into the SDK's RunHooks interface — not function-level decoration
  • Policy-driven: Define tool estimates once in a map, not per-function
  • LLM governance: Every LLM call reserves and commits with real token metrics
  • Pre-run guardrail: /v1/decide check before agent starts — zero tokens on DENY
  • Handoff-aware: Agent handoffs recorded as audit events in the Cycles ledger
  • Automatic heartbeat: TTL extension keeps reservations alive during long operations
  • Fail-safe cleanup: release_pending() frees locked budget when agent runs fail
  • Fail-open by default: Agent continues if Cycles server is unreachable
  • Environment config: CYCLES_BASE_URL + CYCLES_API_KEY for zero-config setup
  • Typed exceptions: BudgetExceededError for precise error handling

Examples

The examples/ directory contains runnable integration examples:

Example Description
basic_budget.py LLM token budget enforcement
tool_governance.py Tool estimate mapping — higher-estimate tools consume more, read-only tools use zero estimate
multi_agent.py Multi-agent handoff with shared budget and pre-run guardrail

See examples/README.md for setup instructions.

Development

pip install -e ".[dev]"

# Lint
ruff check .

# Type check (strict mode)
mypy src/runcycles_openai_agents

# Run tests with coverage (95% threshold enforced in CI)
pytest --cov

CI runs all three checks on Python 3.10 and 3.12 for every push and pull request.

Documentation

License

Apache 2.0

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

runcycles_openai_agents-0.2.1.tar.gz (32.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

runcycles_openai_agents-0.2.1-py3-none-any.whl (18.3 kB view details)

Uploaded Python 3

File details

Details for the file runcycles_openai_agents-0.2.1.tar.gz.

File metadata

  • Download URL: runcycles_openai_agents-0.2.1.tar.gz
  • Upload date:
  • Size: 32.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.13

File hashes

Hashes for runcycles_openai_agents-0.2.1.tar.gz
Algorithm Hash digest
SHA256 068ab0c549144b0844985ee4637bcc464e44c890b05d5077d196a02351e0e14a
MD5 f7a0c4974c8fc7c3afc4b9d9555df49b
BLAKE2b-256 a753aaaffc569db168ecddbcb1235fb22b3d7533b090b8af4e4946d8eab8aa5c

See more details on using hashes here.

Provenance

The following attestation bundles were made for runcycles_openai_agents-0.2.1.tar.gz:

Publisher: python-publish.yml on runcycles/cycles-openai-agents

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file runcycles_openai_agents-0.2.1-py3-none-any.whl.

File metadata

File hashes

Hashes for runcycles_openai_agents-0.2.1-py3-none-any.whl
Algorithm Hash digest
SHA256 457130e338667b817849fa60c30cf5249a896ec05d329314363dbcc47d3afbdf
MD5 1b3457e63f54a6aedef4ca484e6d61b6
BLAKE2b-256 704df9383381d8badacba66d9dc6b71c0856b0b1edfa6ca7ceb374cae64ccd29

See more details on using hashes here.

Provenance

The following attestation bundles were made for runcycles_openai_agents-0.2.1-py3-none-any.whl:

Publisher: python-publish.yml on runcycles/cycles-openai-agents

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page