pub struct SessionSpawnBudget(/* private fields */);Expand description
Session-wide cumulative counter of subagent spawns.
Bounds the total number of subagents spawned over a session’s lifetime, independent of
SubAgentManager::spawn’s existing
max_concurrent (in-flight) and max_spawn_depth (recursion) guardrails — a shallow,
low-concurrency but high-frequency sequential delegation loop trips neither of those.
§Ownership, not a shared handle
Deliberately a plain AtomicUsize newtype — no Arc, no Clone. Nothing in this crate
needs a shared, cloned handle to a budget: SubAgentManager owns one instance as the origin
of truth, and zeph-core’s OrchestrationState owns an independent fallback instance used
only when no manager is wired. Both the manager-side spawn path and the ACP /subagent spawn chokepoint (which never touches SubAgentManager at all) reach whichever instance
applies through an accessor (Agent::session_budget in zeph-core) that hands out a plain
&SessionSpawnBudget reference, never a copy.
This also makes a would-be TOCTOU hazard structurally impossible rather than merely
documented: such a hazard could only arise if SubAgentManager were ever shared (e.g.
behind an Arc) across concurrent tasks, and a future refactor down that path would have to
deliberately reintroduce Clone/Arc here — a change reviewable on its own, rather than one
hiding behind an innocuous .clone() call at some unrelated call site.
§Concurrency
AtomicUsize provides interior mutability so check and
record_spawn work through a shared &self reference (every access
goes through &self accessors, never &mut self), satisfying NFR-001’s atomic-counter
requirement. Every call path that reaches either method — SubAgentManager::spawn/resume,
the orchestration scheduler, and the ACP chokepoint — is serialized behind &mut Agent on
the single agent task, so Relaxed ordering suffices. The check/consume split (budget is
checked at the spawn guard but only consumed at the true commit point, so a rejected or
transiently retried spawn never burns budget it never used) introduces no reachable race
under that serialization.
§Examples
use zeph_subagent::SessionSpawnBudget;
let budget = SessionSpawnBudget::default();
assert_eq!(budget.spawned(), 0);
budget.check(1).expect("budget not yet exhausted");
budget.record_spawn();
assert_eq!(budget.spawned(), 1);
assert!(budget.check(1).is_err(), "cap of 1 must now be exhausted");
// `0` is the unlimited sentinel: check() always succeeds regardless of count.
assert!(budget.check(0).is_ok());Implementations§
Source§impl SessionSpawnBudget
impl SessionSpawnBudget
Sourcepub fn check(&self, max: usize) -> Result<(), SubAgentError>
pub fn check(&self, max: usize) -> Result<(), SubAgentError>
Check the budget without consuming it.
max == 0 is the unlimited sentinel and always succeeds, so callers never need to
duplicate the sentinel check themselves (mirrors
DelegationMode::permits_explicit’s
anti-drift rationale for a check shared across multiple chokepoints).
§Errors
Returns SubAgentError::SessionSpawnLimit when the cumulative spawn count has
already reached max.
Sourcepub fn record_spawn(&self)
pub fn record_spawn(&self)
Record a successful spawn, incrementing the cumulative count by one.
Must be called only at a spawn’s true commit point — see the check/consume split described in the type-level concurrency note.
Trait Implementations§
Source§impl Debug for SessionSpawnBudget
impl Debug for SessionSpawnBudget
Source§impl Default for SessionSpawnBudget
impl Default for SessionSpawnBudget
Source§fn default() -> SessionSpawnBudget
fn default() -> SessionSpawnBudget
Auto Trait Implementations§
impl !Freeze for SessionSpawnBudget
impl RefUnwindSafe for SessionSpawnBudget
impl Send for SessionSpawnBudget
impl Sync for SessionSpawnBudget
impl Unpin for SessionSpawnBudget
impl UnsafeUnpin for SessionSpawnBudget
impl UnwindSafe for SessionSpawnBudget
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
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 moreSource§impl<T> IntoRequest<T> for T
impl<T> IntoRequest<T> for T
Source§fn into_request(self) -> Request<T>
fn into_request(self) -> Request<T>
T in a tonic::Request