mod advance;
mod commit;
pub use commit::CandidateError;
mod inference;
mod reply;
mod runtime;
mod trajectory;
#[derive(Debug, Eq, PartialEq)]
pub enum RamDecision {
Yield(crate::command::Command),
Finish,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum RamProgress {
InferAgain,
Sealed,
}
mod units;
pub use inference::{
InferenceError, InferencePhase, InferenceSession, RequestBinding, RequestSequence,
};
pub use reply::{CallMeta, ModelReply, ReplyError, ToolCall};
pub use runtime::{
prepare, Activation, ActivationError, ActivationPrepareError, ExecutionQuotes, ModelRequest,
RunPolicy,
};
pub use units::{UnitBudget, UnitError};
use crate::{
boundary::Exit,
definition::{Definition, ToolControl},
format::DaslCid,
preparation::{InputBinding, ValidatedInput},
tools::{BoundTool, ToolSet, ToolSetError},
};
use core::fmt;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct EpisodeCounts {
pub model_calls: u64,
pub actions: u64,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct CallCounts {
pub model_calls: u64,
pub local_tool_calls: u64,
pub actions: u64,
pub corrective_turns: u64,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum CallBudgetError {
CounterOverflow,
ModelCallsExhausted,
LocalToolsExhausted,
ActionsExhausted,
CorrectiveTurnsExhausted,
DefinitionMismatch,
Tools(ToolSetError),
}
impl fmt::Display for CallBudgetError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Self::CounterOverflow => "Episode call counter overflow",
Self::ModelCallsExhausted => "model-call count capacity exhausted",
Self::LocalToolsExhausted => "Continue-tool count capacity exhausted",
Self::ActionsExhausted => "Yield-action count capacity exhausted",
Self::CorrectiveTurnsExhausted => {
"corrective-turn count capacity exhausted after invalid tool decisions"
}
Self::DefinitionMismatch => "call budget was derived from another Definition",
Self::Tools(error) => return error.fmt(f),
})
}
}
impl From<ToolSetError> for CallBudgetError {
fn from(error: ToolSetError) -> Self {
Self::Tools(error)
}
}
impl core::error::Error for CallBudgetError {
fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
match self {
Self::Tools(error) => Some(error),
_ => None,
}
}
}
#[derive(Debug, Eq, PartialEq)]
pub struct CallBudget {
binding: InputBinding,
definition: DaslCid,
accepted: EpisodeCounts,
capacity: CallCounts,
used: CallCounts,
}
impl CallBudget {
pub fn from_input(input: &ValidatedInput<'_>) -> Result<Self, CallBudgetError> {
let snapshot = input.snapshot();
let definition = snapshot.definition();
let policy = definition.limits();
let caller = input.caller_limits();
let grant = input.input().budget_admission();
let mut accepted = EpisodeCounts {
model_calls: 0,
actions: 0,
};
if let Some(episode) = snapshot.current_episode() {
for cid in episode.boundaries() {
let boundary = snapshot.boundary(cid).expect("restored Boundary");
accepted.model_calls = accepted
.model_calls
.checked_add(boundary.usage().model_calls)
.ok_or(CallBudgetError::CounterOverflow)?;
if matches!(boundary.exit(), Exit::AwaitAction(_)) {
accepted.actions = accepted
.actions
.checked_add(1)
.ok_or(CallBudgetError::CounterOverflow)?;
}
}
}
let capacity = CallCounts {
model_calls: policy
.max_model_calls_per_activation
.min(caller.max_model_calls_per_activation)
.min(
policy
.max_model_calls_per_episode
.min(caller.max_model_calls_per_episode)
.saturating_sub(accepted.model_calls),
)
.min(grant.remaining_model_calls),
actions: policy
.max_actions_per_episode
.min(caller.max_actions_per_episode)
.saturating_sub(accepted.actions)
.min(grant.remaining_actions),
local_tool_calls: policy
.max_local_tools_per_activation
.min(caller.max_local_tools_per_activation),
corrective_turns: policy.max_corrective_turns.min(caller.max_corrective_turns),
};
Ok(Self {
binding: input.binding(),
definition: definition.cid(),
accepted,
capacity,
used: CallCounts {
model_calls: 0,
local_tool_calls: 0,
actions: 0,
corrective_turns: 0,
},
})
}
pub fn binding(&self) -> InputBinding {
self.binding
}
pub fn accepted_episode_counts(&self) -> EpisodeCounts {
self.accepted
}
pub fn capacity(&self) -> CallCounts {
self.capacity
}
pub fn used(&self) -> CallCounts {
self.used
}
pub fn remaining(&self) -> CallCounts {
CallCounts {
model_calls: self.capacity.model_calls - self.used.model_calls,
local_tool_calls: self.capacity.local_tool_calls - self.used.local_tool_calls,
actions: self.capacity.actions - self.used.actions,
corrective_turns: self.capacity.corrective_turns - self.used.corrective_turns,
}
}
pub fn episode_counts(&self) -> EpisodeCounts {
EpisodeCounts {
model_calls: self.accepted.model_calls + self.used.model_calls,
actions: self.accepted.actions + self.used.actions,
}
}
pub fn debit_model_call(&mut self) -> Result<(), CallBudgetError> {
debit(
&mut self.used.model_calls,
self.capacity.model_calls,
CallBudgetError::ModelCallsExhausted,
)
}
pub fn debit_control(&mut self, control: ToolControl) -> Result<(), CallBudgetError> {
match control {
ToolControl::Continue => debit(
&mut self.used.local_tool_calls,
self.capacity.local_tool_calls,
CallBudgetError::LocalToolsExhausted,
),
ToolControl::Yield => debit(
&mut self.used.actions,
self.capacity.actions,
CallBudgetError::ActionsExhausted,
),
ToolControl::Finish => Ok(()),
}
}
pub fn debit_corrective_turn(&mut self) -> Result<(), CallBudgetError> {
debit(
&mut self.used.corrective_turns,
self.capacity.corrective_turns,
CallBudgetError::CorrectiveTurnsExhausted,
)
}
pub fn count_enabled_tools<'a>(
&'a self,
tools: &'a ToolSet<'_>,
definition: &'a Definition,
) -> Result<impl Iterator<Item = BoundTool<'a>> + 'a, CallBudgetError> {
if self.definition != definition.cid() {
return Err(CallBudgetError::DefinitionMismatch);
}
let remaining = self.remaining();
Ok(tools
.tools(definition)?
.filter(move |tool| match tool.descriptor().control {
ToolControl::Continue => remaining.local_tool_calls > 0,
ToolControl::Yield => remaining.actions > 0,
ToolControl::Finish => true,
}))
}
}
fn debit(used: &mut u64, capacity: u64, error: CallBudgetError) -> Result<(), CallBudgetError> {
if *used == capacity {
return Err(error);
}
*used += 1;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn full_width_debit_reaches_max_exactly_and_refuses_overflow_without_mutation() {
let mut used = u64::MAX - 1;
assert_eq!(
debit(&mut used, u64::MAX, CallBudgetError::ModelCallsExhausted),
Ok(())
);
assert_eq!(used, u64::MAX);
assert_eq!(
debit(&mut used, u64::MAX, CallBudgetError::ModelCallsExhausted),
Err(CallBudgetError::ModelCallsExhausted)
);
assert_eq!(used, u64::MAX);
let mut zero = 0;
assert_eq!(
debit(&mut zero, 0, CallBudgetError::ActionsExhausted),
Err(CallBudgetError::ActionsExhausted)
);
assert_eq!(zero, 0);
}
}