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
Want a real LLM example?
examples/async_openai_completion.rswires the samewith_cyclesflow againstasync-openai, threading the response'susage.total_tokensback into the commit. Run it withcargo run --example async_openai_completion— requiresOPENAI_API_KEYin the env plus a reachable Cycles server, a tenant API key, and aTOKENS-denominated budget at the scope the example reserves against.
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;
Commit retry
If the attempt inside guard.commit(...) fails with a retryable error — a
transport failure, a 5xx server error, or an error code the protocol
classifies as transient (e.g. LIMIT_EXCEEDED rate limiting; unrecognized
error codes are treated as transient for forward compatibility, see
Error::is_retryable) — and retry_enabled is set (the default), the commit
is retried inline with exponential backoff (retry_initial_delay ×
retry_multiplier, capped at retry_max_delay, up to retry_max_attempts
attempts). Retries reuse the original request — same idempotency key — so a
commit that already landed server-side cannot double-charge, and the
reservation heartbeat keeps extending the TTL until the outcome is final, so
the reservation cannot expire mid-retry.
The result of commit() is final: Ok means the spend was recorded, Err
means no further commit attempt will ever be made — safe to compensate.
Under a persistent outage the call blocks for the full backoff schedule;
tune the knobs or set retry_enabled(false) for fail-fast single-attempt
commits. Env knobs: CYCLES_RETRY_ENABLED, CYCLES_RETRY_MAX_ATTEMPTS,
CYCLES_RETRY_INITIAL_DELAY, CYCLES_RETRY_MULTIPLIER,
CYCLES_RETRY_MAX_DELAY.
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