Skip to main content

BudgetPool

Struct BudgetPool 

Source
pub struct BudgetPool { /* private fields */ }
Expand description

A token allowance several runs draw on at once.

§Shared mutable state, deliberately

Everything else a run is configured with is an immutable value: a RunSpec derived from another spec leaves the original alone, and two runs minted from one spec cannot influence each other. A pool is the opposite by construction — twenty concurrent runs drawing on one figure have to see each other’s spending, or it is not one figure. So this is the sanctioned exception: the handle is cheap to clone and every clone is the same pool, because the allowance lives behind an Arc and moves under an atomic. Clone here means “another handle”, never “another allowance”, and PartialEq says so — two handles compare equal when they meter the same counter against the same limit, not when they happen to name the same number. A capped view shares the counter but carries its own tighter limit, so it is deliberately not equal to its parent.

§There is no reservation, and none is possible

A run’s token usage is only known once a round has streamed in full, so nothing can be set aside for a run in advance and reconciled later without guessing at the size of the guess. This does not guess. The pool is the counter mentra keeps: attaching one to a run hands mentra the same AtomicU64 every other run on this pool reports into (RunOptions::token_usage, which mentra documents as the way to alias one run’s accounting to another’s), together with the pool’s limit as that run’s token_budget. Every round of every drawing run adds its input+output to the one total, and every round boundary in every drawing run compares that total to the limit. So remaining is not an estimate that a settlement later corrects — it is the number the turns are being stopped against, read live.

A run that spends nothing therefore costs the pool nothing. Nothing is held back on its behalf and nothing has to be returned.

§How much it can overshoot

A pool bounds what a run starts, not what it finishes. Usage is known only at a round boundary, so the round that crosses the line always completes — that is the softness Bounds::token_budget already documents — and with N runs in flight, N of them can be mid-round when the line is crossed. State it as: the pool lands at up to limit plus one round from each run that was running when the limit was reached. For a sequential caller that is one round; for a twenty-way fan-out it is twenty. If that tail matters, cap each run with RunSpec::with_token_budget as well, or fan out less widely — but do not read limit as a ceiling.

§What it sees, and what it does not

Delegated work is inside the pool, whichever door it came through. mentra’s task intrinsic and basis’s own spawn (ADR-0016, the door the model actually holds) both drive the subagent on the parent run’s RunOptions::child, which carries the same accounting handle — this pool’s counter — and the same bound, so a fan-out whose runs delegate draws on one figure at every depth rather than spending beside it. What the two doors do NOT share is the tally: task relays its child’s usage reports onto the parent’s stream, so RunUsage agrees with what stopped the turn — but the relay is pub(crate) in mentra, spawn cannot reach it, and a spawn-delegating run’s RunUsage under-reports what the pool honestly charged. The bound is airtight; the receipt is not. Named as an open upstream candidate in the REDESIGN ledger. Before mentra 0436bae none of the bounding held either: task ran its child on fresh options, and a delegating fan-out spent more than this pool would ever admit to.

The edge that survives is a refusal rather than an overrun. A delegation issued once the pool is already crossed inherits an allowance with nothing in it, does zero rounds, and fails the tool call visibly instead of returning an empty success — the delegating side of the same round-boundary softness described above.

What genuinely stays outside is RunUsage’s caveat and not a structural one: this counts what providers report. One that reports nothing spends nothing as far as the pool is concerned.

§Running out

A turn that draws on a pool with nothing left is refused — RunError::BudgetExhausted — before the prompt is sent, before the header is emitted, and before anything is committed to the conversation. It is a decision, not a failure of the work, and it is stated once at the point where money would be spent, so a run minted while the pool was full and driven after it drained still gets it.

The alternative was to let it through with a zero budget, and it is worth recording why not. mentra checks reported >= budget, so Some(0) is already crossed before the first round: the run ends gracefully having done nothing, and because it owes its caller a final assistant message that never arrived, it surfaces as EmptyAssistantResponse — a provider-shaped error for an accounting decision, with the user’s prompt left committed to the transcript. The report does name Bound::TokenBudget now that mentra records which bound ended a run, so the error is at least not mistaken for a broken provider; the wasted turn and the stranded prompt are what refusing still avoids. basis/tests/budget.rs pins that upstream behavior so the reasoning stays checkable.

A run already underway when the pool drains is stopped rather than refused: it ends at its next round boundary, keeps what it committed, and reports Bound::TokenBudget. Two answers on purpose — the refusal says nothing was spent, the bound says something was.

A caller that would rather not mint at all asks first, since both readings are honest live numbers:

let pool = BudgetPool::new(500_000);
while pool.remaining() > 20_000 {
    // mint another run
}

§Using one

use basis::{BudgetPool, CollectingSink, Workspace};

let workspace = Workspace::open("/repo").await?;
let pool = BudgetPool::new(500_000);

// Two runs, one allowance. Neither knows about the other; both stop when
// the pair of them has spent 500k.
let mut first = workspace.prepare(pool.spec("review the tests"))?;
let mut second = workspace.prepare(pool.spec("review the docs"))?;
let (a, b) = tokio::join!(
    first.execute(CollectingSink::default()),
    second.execute(CollectingSink::default()),
);

println!("the job cost {} of {}", pool.spent(), pool.limit());

Implementations§

Source§

impl BudgetPool

Source

pub fn new(limit: u64) -> Self

A pool with limit tokens in it, input plus output.

The same figure RunUsage::total_tokens reports and mentra enforces: cache reads and cache writes are counted by neither, because they are priced differently everywhere and a total that mixed them would answer no question exactly.

Source

pub const fn limit(&self) -> u64

What this pool was created with. Never moves.

Source

pub fn with_token_allowance(&self, additional_tokens: u64) -> Self

A view that allows at most additional_tokens more spending from now.

The view shares this pool’s counter: spending through this pool, the view, or any sibling handle consumes the same allowance. Its stopping threshold is the smaller of this pool’s limit and the current spend plus additional_tokens, so deriving a view can tighten an allowance but can never extend its parent. Addition saturates at u64::MAX.

This is a live bound, not a reservation. Concurrent sibling spending therefore leaves fewer tokens for work using the view.

Source

pub fn spent(&self) -> u64

What every run drawing on this pool has reported spending so far.

Live: a fan-out still in flight moves this between two reads.

Source

pub fn remaining(&self) -> u64

What is left, saturating at zero.

Zero rather than a negative number or a wrap, because a pool is overspendable — the round that crosses the line finishes, and a caller can record more than was ever granted. “Nothing left” is the honest answer to all of those; spent against limit is where the size of the overshoot is legible.

Source

pub fn is_exhausted(&self) -> bool

Whether a turn drawing on this pool would be refused.

True the moment reported spending reaches the limit — the same comparison mentra makes at a round boundary, so this and the thing that stops a turn cannot disagree.

Source

pub fn spec(&self, prompt: impl Into<String>) -> RunSpec

One run’s worth of intent, bounded by this pool.

The shorthand the fan-out is written in: workspace.prepare(pool.spec(p)) mints a run every turn of which draws here.

Source

pub fn bounds(&self) -> TurnOptions

Turn options that bound a single call to this pool and say nothing else.

For the turns a spec does not cover: a second prompt on a conversation, or a call that also carries a stop token — TurnOptions::with_budget composes with the rest.

Source

pub fn record(&self, usage: RunUsage) -> u64

Charges the pool for spending it did not meter itself, returning the new total.

Not a settlement. A run this pool bounded has already been counted, round by round, as it ran — passing its RunReport::usage here would bill the job twice, and so would passing a delegated subagent’s usage, which now reports into this same counter on its own. This is for work that spent against the same allowance without ever drawing on the pool: a run bounded some other way, or a call the host made itself.

Saturates rather than wrapping, so an absurd figure cannot roll the counter over and hand the job a fresh allowance.

Trait Implementations§

Source§

impl Clone for BudgetPool

Source§

fn clone(&self) -> BudgetPool

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl Debug for BudgetPool

Prints the figures rather than the handle, so a RunSpec or TurnOptions carrying a pool debugs into something a caller can read.

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl Eq for BudgetPool

Source§

impl PartialEq for BudgetPool

Accounting identity plus bound: two handles are equal when they share one counter and stop it at the same limit.

Comparing the numbers instead would make two independent 500k allowances equal while they are both untouched and unequal a moment later, which describes no useful question. Comparing only the counter would make a tighter BudgetPool::with_token_allowance view equal to its parent even though the two stop at different thresholds. This definition lets RunSpec keep its derived PartialEq: two specs are equal only when their accounting and bound are both the same.

Source§

fn eq(&self, other: &Self) -> bool

Equality operator ==. Read more
1.0.0 (const: unstable) · Source§

fn ne(&self, other: &Rhs) -> bool

Inequality operator !=. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<Q, K> Equivalent<K> for Q
where Q: Eq + ?Sized, K: Borrow<Q> + ?Sized,

Source§

fn equivalent(&self, key: &K) -> bool

Checks if this value is equivalent to the given key. Read more
Source§

impl<Q, K> Equivalent<K> for Q
where Q: Eq + ?Sized, K: Borrow<Q> + ?Sized,

Source§

fn equivalent(&self, key: &K) -> bool

Compare self to key and return true if they are equal.
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self>

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self>

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> Pointable for T

Source§

const ALIGN: usize

The alignment of pointer.
Source§

type Init = T

The type for initializers.
Source§

unsafe fn init(init: <T as Pointable>::Init) -> usize

Initializes a with the given initializer. Read more
Source§

unsafe fn deref<'a>(ptr: usize) -> &'a T

Dereferences the given pointer. Read more
Source§

unsafe fn deref_mut<'a>(ptr: usize) -> &'a mut T

Mutably dereferences the given pointer. Read more
Source§

unsafe fn drop(ptr: usize)

Drops the object pointed to by the given pointer. Read more
Source§

impl<T> PolicyExt for T
where T: ?Sized,

Source§

fn and<P, B, E>(self, other: P) -> And<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow only if self and other return Action::Follow. Read more
Source§

fn or<P, B, E>(self, other: P) -> Or<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow if either self or other returns Action::Follow. Read more
Source§

impl<T> Read<Exclusive, BecauseExclusive> for T
where T: ?Sized,

Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<V, T> VZip<V> for T
where V: MultiLane<T>,

Source§

fn vzip(self) -> V

Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self>
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self>

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more