Skip to main content

budgetkernel/
lib.rs

1//! # budgetkernel
2//!
3//! Every system that consumes resources on behalf of a caller needs to
4//! know when to stop. LLM agents burn tokens. Task runners burn CPU
5//! time. API gateways burn request quotas. The stopping logic is always
6//! the same: accumulate, compare, decide.
7//!
8//! `budgetkernel` isolates that decision into a deterministic budget
9//! accounting kernel with zero heap allocation on the hot path. Declare
10//! budgets across fixed
11//! dimensions, charge them at runtime boundaries, and receive one of
12//! three verdicts: [`Verdict::Continue`], [`Verdict::Warn`], or
13//! [`Verdict::Exhausted`].
14//!
15//! The kernel never reads a clock, never touches I/O, and never decides
16//! what your program should do after a verdict. The caller owns time,
17//! logging, persistence, and policy. The kernel owns exactly one thing:
18//! given what has been spent and what is allowed, what should happen
19//! next?
20//!
21//! ## Example
22//!
23//! ```rust
24//! use budgetkernel::{Budget, Dim, Verdict};
25//!
26//! fn main() -> Result<(), ()> {
27//!     let mut budget = Budget::builder()
28//!         .limit_with_warn(Dim::Tokens, 100_000, 80_000)
29//!         .limit_with_warn(Dim::Millis, 30_000, 27_000)
30//!         .limit(Dim::Calls, 50)
31//!         .build()
32//!         .map_err(|_| ())?;
33//!
34//!     match budget.charge(Dim::Tokens, 12_500).map_err(|_| ())? {
35//!         Verdict::Continue => {}
36//!         Verdict::Warn(_dim) => {}
37//!         Verdict::Exhausted(_dim) => {}
38//!     }
39//!
40//!     Ok(())
41//! }
42//! ```
43//!
44//! No background threads. No timers. No global state. The same inputs
45//! produce the same verdict on every machine, in every test, under Miri.
46//!
47//! ## Design discipline
48//!
49//! - **No heap allocation on the hot path.** All state lives on the stack
50//!   in fixed-size arrays sized by [`MAX_DIMS`].
51//! - **No clock, no I/O, no syscalls.** The caller supplies elapsed time
52//!   as a `u64` when charging the time dimension. Determinism follows.
53//! - **No panics.** Every fallible operation returns a `Result` or a
54//!   `Verdict` variant. Arithmetic is saturating throughout.
55//! - **Bounded termination.** Every public function is `O(MAX_DIMS)` or
56//!   better, with `MAX_DIMS` fixed at compile time.
57//! - **`no_std` compatible.** The `std` feature enables convenience
58//!   impls but is not required for core use.
59//! - **Adapter-layer philosophy.** The kernel does the accounting; the
60//!   caller owns clocks, pricing, logging, and policy.
61//!
62//! ## Features
63//!
64//! - `std` (default): enables `std::error::Error` impls for error types.
65//! - `safe-map`: replaces the `MaybeUninit`-based internal map with a
66//!   fully-safe variant. Identical semantics, slightly higher per-call
67//!   initialization cost. No `unsafe` anywhere in the crate when this
68//!   feature is active.
69//!
70//! ## Safety and security model
71//!
72//! The only current unsafe boundary is the internal fixed-map
73//! implementation. See the
74//! [security model](https://github.com/Qarait/budgetkernel/blob/master/docs/SECURITY_MODEL.md)
75//! for invariants, lint posture, threat model, and verification details.
76//!
77//! ## Non-goals
78//!
79//! This crate does not and will not:
80//!
81//! - Refill budgets over time (that is a rate limiter's job).
82//! - Persist ledger state (the host owns durability).
83//! - Coordinate across processes or machines.
84//! - Provide async APIs.
85//! - Allow dynamic dimension registration (the dimension set is
86//!   compile-time fixed; see [`Dim`]).
87
88#![cfg_attr(not(feature = "std"), no_std)]
89#![deny(
90    clippy::unwrap_used,
91    clippy::panic,
92    clippy::expect_used,
93    clippy::indexing_slicing,
94    unsafe_op_in_unsafe_fn,
95    missing_docs
96)]
97#![warn(clippy::pedantic, clippy::nursery)]
98
99pub mod budget;
100pub mod dim;
101pub mod error;
102pub(crate) mod fixed_map;
103pub mod verdict;
104
105pub use budget::{Budget, BudgetBuilder};
106pub use dim::{Dim, MAX_DIMS};
107pub use error::{BuilderError, ChargeError};
108pub use verdict::Verdict;