pub struct Budget {
pub max_steps: Option<usize>,
pub max_effects: Option<usize>,
pub max_tokens: Option<u64>,
pub max_minor_units: Option<u64>,
pub max_replans: Option<u32>,
pub max_wallclock_secs: Option<u64>,
pub max_parallel_steps: Option<usize>,
pub max_denials: Option<u32>,
pub max_egress_bytes: Option<u64>,
pub max_call_tokens: Option<u64>,
pub max_call_minor_units: Option<u64>,
}Expand description
What a run is allowed to consume.
Every limit is optional; Budget::unlimited() is a legitimate choice for a
run that only touches free, local effects. It is not the default, because
“nobody set a limit” and “somebody decided no limit was needed” should not
look the same in a config file.
§A ceiling gates every effect, not only the metered ones
Worth stating once, because the field names invite the opposite reading:
admit_effect evaluates all of these limits before
every effect, whatever kind it is. A run holding a token ceiling passes it
on the way to a read-only tool call, and a run that has reached any one
ceiling performs no further operation of any kind. So max_tokens is not
“what the model may spend” — it is a gate the whole run walks through.
The corollary is that zero is not a useful value for any ceiling here except
max_replans and max_denials: at
zero the limit is reached before the run starts, and the agent can never do
anything at all. Refused on both paths that can carry one — the manifest at
parse, and RuntimeBuilder::budget at build — from the single rule in
bricked_ceiling.
Fields§
§max_steps: Option<usize>Plan nodes this run may execute, checked before each one starts.
Zero permits no step at all, so a run carrying it cannot begin.
max_effects: Option<usize>Externally visible operations, of every kind.
The blunt instrument that stops a loop nobody predicted, independent of what each operation happens to cost — a free local read costs one here exactly as a model completion does. Zero permits no effect at all.
max_tokens: Option<u64>Metered units the run may consume in total.
Compared before every effect, including the effects that consume no tokens, so this bounds when the run stops rather than only what the model costs. Zero refuses the first effect of any kind.
max_minor_units: Option<u64>Money in minor units the run may spend in total.
Gates every effect, exactly like max_tokens:
a free effect still has to pass it. Zero refuses the first one.
max_replans: Option<u32>How many times a run may change its plan.
A run that replans without bound is a run that has stopped making progress and started thrashing, and the ceiling is what turns that from an unbounded spend into a reported fault.
Zero is meaningful here, unlike the ceilings above: it says the plan the run started with is the plan it finishes with, and a run that never replans is unaffected by it.
max_wallclock_secs: Option<u64>Wall-clock ceiling.
Costs one journaled clock read per step boundary when set — see the
module docs. Checked against
Consumed::elapsed_secs, which counts second
boundaries rather than measuring a duration and starts at zero, so a zero
ceiling refuses the first step.
max_parallel_steps: Option<usize>How many of a plan’s ready steps may run at once.
A ready set is every node whose predecessors are done, so nothing in it depends on anything else in it and running them concurrently is the point of writing a graph rather than a list. What that width costs is paid outside this process — connections, provider rate limits, and the metered ceilings above, which are checked before an operation and billed after it, so each step in flight may be holding one operation’s worth of unbilled spend.
Absent means the plan’s own width is the bound, which is the right default for a graph the embedder wrote and the wrong one for a graph anything else may widen. Zero permits no step at all.
max_denials: Option<u32>How many times this run may be refused before it is stopped.
A run that keeps being refused is probing. Refusals carry a uniform
message precisely so a model cannot tell one from another
(REFUSED), but the refused/allowed bit itself
still leaks one bit per attempt, and nothing short of fabricating success
removes that. What bounds the channel is bounding the attempts.
Both refusal paths count, and they behave differently. A sink
refusal — an untrusted value in a protected field, a sensitivity over a
ceiling — comes back to a tool-calling model as REFUSED and the loop
continues, so those accumulate and this is what stops them. An engine
denial is not model-facing at all: in that same loop it ends the run
outright, which is a stricter bound than any ceiling, and it accumulates
only where a run’s own code catches
StepError::Denied and carries on.
Counting only the second would leave the ceiling naming a channel it did
not reach.
It is an operational ceiling as much as a security one: a run stuck in a denial loop has stopped making progress, exactly like one that replans without bound.
Zero is meaningful here too, and for a different reason than
max_replans: this one is counted after the
refusal and compared with >, so zero says the first refusal ends the
run. A run nothing refuses never notices it.
max_egress_bytes: Option<u64>Bytes this run may send into sinks, in total.
Volume is the axis a label does not have. Ten thousand records marked
Internal pass every gate one record passes, and every ceiling above is
a cost control that bounds work: an extraction sized just under an
effect count is invisible to all of them. This bounds the quantity that
left.
Measured as the canonical size of each effect’s outbound value — a tool
call’s arguments, a model call’s prompt, a peer call’s payload — which is
the figure EffectStarted.outbound_bytes records. An effect binding no
outbound value costs nothing here, so a run of pure reads is unaffected.
Exact, unlike every metered ceiling above. A token cost is unknown until the call returns, so those refuse once consumption has reached the limit and the run overshoots by at most one operation. This size is known before dispatch, so the effect that would cross the ceiling is the effect refused: nothing over the limit is ever sent.
Zero is meaningful, like max_denials and unlike
the metered ceilings: it says this run may read and may not send, which
is a coherent thing to ask of an agent. So it is not a bricked ceiling.
What it is not is an anomaly detector. Forty times the median for this capability is a threshold a deployment sets against its own traffic, and the journaled figure is what makes that an ordinary query. A ceiling bounds the worst case; the figure catches the case that stayed under it.
max_call_tokens: Option<u64>The most tokens one operation of this run can report.
Not a ceiling the ledger checks — a metered cost is unknown until the
call returns — but the size of the overshoot the ceilings above permit:
a run stops once max_tokens is reached, so it can
end one operation past it per step in flight. A tenant’s spend quota
reserves the ceiling plus that overshoot at admission, and this is
the figure it reserves per step, so under a token quota a run that
declares none is refused.
A manifest derives it from its model roles (max_input_tokens plus
max_tokens per call), and a model call reporting more input than its
role declares fails. An embedder’s own effects cannot be inspected, so
for those it is a declaration the reservation is only as exact as —
the same footing as a price.
max_call_minor_units: Option<u64>The most money, in minor units, one operation of this run can report.
max_call_tokens for
max_minor_units: the per-step overshoot a
tenant’s money quota reserves beside the ceiling. A manifest derives it
from each role’s per-call token maxima at the role’s declared price.
Implementations§
Source§impl Budget
impl Budget
Sourcepub const fn bricked_ceiling(&self) -> Option<&'static str>
pub const fn bricked_ceiling(&self) -> Option<&'static str>
The first ceiling set to zero, if any — the field name, for a message.
Zero is not a small limit here; it is a limit already reached. Every ceiling this reports is compared before the work and against every effect of every kind, so at zero the run is refused its first operation: a read-only tool call, a local lookup, an agent that declares no models at all. Such a plane does not run once and stop — it fails identically on every run it will ever make, which makes it a wiring mistake rather than a budget.
Three are excluded, because zero is a coherent instruction for each:
max_replans says do not replan,
max_denials says the first refusal ends the
run, and max_egress_bytes says this run
may read and may not send. A run that never replans, is never refused,
or sends nothing is unaffected by its own — which is what separates
them from a ceiling that refuses the first operation of any kind.
pub const fn steps(self, n: usize) -> Self
pub const fn effects(self, n: usize) -> Self
pub const fn tokens(self, n: u64) -> Self
pub const fn minor_units(self, n: u64) -> Self
pub const fn replans(self, n: u32) -> Self
Sourcepub const fn denials(self, n: u32) -> Self
pub const fn denials(self, n: u32) -> Self
How many refusals this run may accrue — sink and engine alike.
Sourcepub const fn egress_bytes(self, n: u64) -> Self
pub const fn egress_bytes(self, n: u64) -> Self
How many bytes this run may send into sinks.
Sourcepub const fn call_tokens(self, n: u64) -> Self
pub const fn call_tokens(self, n: u64) -> Self
The most tokens one operation may report. See
max_call_tokens.
Sourcepub const fn call_minor_units(self, n: u64) -> Self
pub const fn call_minor_units(self, n: u64) -> Self
The most money one operation may report. See
max_call_minor_units.
pub const fn wallclock_secs(self, n: u64) -> Self
Sourcepub const fn parallel_steps(self, n: usize) -> Self
pub const fn parallel_steps(self, n: usize) -> Self
Bound the width of a ready set. See
max_parallel_steps.
Sourcepub const fn parallelism(&self) -> usize
pub const fn parallelism(&self) -> usize
How many ready steps may be dispatched at once — the plan’s own width when nothing narrows it.
pub const fn tracks_wallclock(&self) -> bool
Trait Implementations§
impl Copy for Budget
Source§impl<'de> Deserialize<'de> for Budget
impl<'de> Deserialize<'de> for Budget
Source§fn deserialize<__D>(__deserializer: __D) -> Result<Self, __D::Error>where
__D: Deserializer<'de>,
fn deserialize<__D>(__deserializer: __D) -> Result<Self, __D::Error>where
__D: Deserializer<'de>,
impl Eq for Budget
impl StructuralPartialEq for Budget
Auto Trait Implementations§
impl Freeze for Budget
impl RefUnwindSafe for Budget
impl Send for Budget
impl Sync for Budget
impl Unpin for Budget
impl UnsafeUnpin for Budget
impl UnwindSafe for Budget
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
Source§impl<T> CloneToUninit for Twhere
T: Clone,
impl<T> CloneToUninit for Twhere
T: Clone,
impl<T> DeserializeOwned for Twhere
T: for<'de> Deserialize<'de>,
Source§impl<Q, K> Equivalent<K> for Q
impl<Q, K> Equivalent<K> for Q
Source§fn equivalent(&self, key: &K) -> bool
fn equivalent(&self, key: &K) -> bool
key and return true if they are equal.Source§impl<T> Instrument for T
impl<T> Instrument for T
Source§fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
Source§fn in_current_span(self) -> Instrumented<Self> ⓘ
fn in_current_span(self) -> Instrumented<Self> ⓘ
Source§impl<T> IntoEither for T
impl<T> IntoEither for T
Source§fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
self into a Left variant of Either<Self, Self>
if into_left is true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read moreSource§fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
self into a Left variant of Either<Self, Self>
if into_left(&self) returns true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read more