Cycles Rust Client — Runtime Authority for AI Agents (Spend, Actions, Audit)
Tokio-native Rust client for the Cycles protocol — runtime authority over autonomous AI agents. Cycles enforces hard limits on three things observation alone can't fix:
- Spend — reserve-commit budget enforcement that stops runaway LLM cost before the next call, not after the invoice arrives.
- Risky actions — three-way decisions (
Allow/AllowWithCaps/Deny) with caps for tool denylists, max tokens, max steps, and cooldowns. The client enforces caps before the agent acts. - Audit gaps — every reservation, commit, release, and decision is a signed event. Compliance, incident review, and per-agent attribution come for free, not as a separate logging project.
This crate implements the reserve-execute-commit lifecycle with an idiomatic Rust API built around RAII guards, ownership semantics, and Send + Sync concurrency. Same wire protocol as the Python, TypeScript, and Spring Boot clients — switch languages without changing the server.
Installation
[]
= "0.2"
Unit must match the budget. The
Amountyou pass toreserve,with_cycles,decide, orcreate_eventmust be in the same unit as the active budget at the target scope. The server indexes budgets by(scope, unit), so reservingAmount::tokens(…)against aUSD_MICROCENTSbudget returns a 404 "Budget not found for provided scope" even though the scope exists. The client enriches such 404s with the unit that was sent to make the mismatch obvious.
Quick Start — Automatic Lifecycle (with_cycles)
Like Python's @cycles decorator or TypeScript's withCycles. Reserve, execute,
and commit/release are handled automatically:
use ;
async
# async
Manual Control — RAII Guard
For streaming, multi-step workflows, or when you need full control:
use ;
async
Design
The Rust client is not a port — it is designed from the ground up around Rust's type system and ownership model:
| Feature | How |
|---|---|
| No double-commit | commit(self) consumes the guard — compile error to reuse |
| No forgotten reservations | #[must_use] warns if guard is ignored |
| Auto-cleanup | Drop does best-effort release via tokio::spawn |
| Type-safe IDs | ReservationId, IdempotencyKey newtypes prevent mixups |
| Forward-compatible | #[non_exhaustive] enums for protocol evolution |
| Zero mapper code | serde with rename_all handles wire format natively |
RAII Guard
The ReservationGuard gives manual control over the lifecycle. It holds a live
reservation and auto-extends TTL via a background heartbeat. The guard IS the
context — no thread-locals or task-locals needed.
# use ;
# async
Low-Level Client
For full control, use the client methods directly:
# use ;
# async
Error Handling
Errors use pattern matching:
use Error;
#
Configuration
From code
# use CyclesClient;
let client = builder
.tenant
.workspace
.connect_timeout
.read_timeout
.retry_enabled
.retry_max_attempts
.build;
From environment
# use ;
// Reads CYCLES_BASE_URL, CYCLES_API_KEY, CYCLES_TENANT, etc.
let config = from_env.expect;
let client = new;
Features
| Feature | Default | Description |
|---|---|---|
rustls-tls |
Yes | Use rustls for TLS |
native-tls |
No | Use platform-native TLS |
blocking |
No | Synchronous blocking client |
License
Apache-2.0