disyn-runtime 0.1.0

Budget manager, telemetry, and shell executor for the disyn agent pipeline
Documentation
use disyn_core::types::{CostEstimate, ResourceUsage};

pub struct BudgetManager {
    max_tokens: u64,
    max_neural_replans: u32,
    max_repair_attempts: u32,
    used_tokens: u64,
    neural_replans: u32,
    repair_attempts: u32,
    max_neural_tokens: u64,
    used_neural_tokens: u64,
    used_symbolic_tokens: u64,
}

impl BudgetManager {
    pub fn new(
        max_tokens: u64,
        max_neural_replans: u32,
        max_repair_attempts: u32,
        max_neural_tokens: u64,
    ) -> Self {
        Self {
            max_tokens,
            max_neural_replans,
            max_repair_attempts,
            used_tokens: 0,
            neural_replans: 0,
            repair_attempts: 0,
            max_neural_tokens,
            used_neural_tokens: 0,
            used_symbolic_tokens: 0,
        }
    }

    pub fn can_afford(&self, estimate: &CostEstimate) -> bool {
        let needed = u64::from(estimate.input_tokens) + u64::from(estimate.output_tokens);
        self.used_tokens + needed <= self.max_tokens
    }

    pub fn can_afford_neural(&self, estimate: &CostEstimate) -> bool {
        let needed = u64::from(estimate.input_tokens) + u64::from(estimate.output_tokens);
        self.used_neural_tokens + needed <= self.max_neural_tokens
            && self.used_tokens + needed <= self.max_tokens
    }

    pub fn record(&mut self, usage: &ResourceUsage) {
        self.used_tokens += usage.total_tokens;
        self.used_neural_tokens += usage.neural_tokens;
        self.used_symbolic_tokens += usage.symbolic_tokens;
    }

    pub fn can_repair(&self) -> bool {
        self.repair_attempts < self.max_repair_attempts
    }

    pub fn record_repair(&mut self) {
        self.repair_attempts += 1;
    }

    pub fn can_replan(&self) -> bool {
        self.neural_replans < self.max_neural_replans
    }

    pub fn record_replan(&mut self) {
        self.neural_replans += 1;
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn budget_starts_with_capacity() {
        let b = BudgetManager::new(1000, 1, 3, 500);
        assert!(b.can_afford(&CostEstimate {
            class: None,
            input_tokens: 500,
            output_tokens: 200,
        }));
    }

    #[test]
    fn budget_rejects_when_exceeded() {
        let mut b = BudgetManager::new(100, 1, 3, 100);
        b.record(&ResourceUsage {
            total_tokens: 90,
            symbolic_tokens: 0,
            neural_tokens: 90,
            wall_time_ms: 100,
        });
        assert!(!b.can_afford(&CostEstimate {
            class: None,
            input_tokens: 50,
            output_tokens: 50,
        }));
    }

    #[test]
    fn replan_attempts_decrement() {
        let mut b = BudgetManager::new(1000, 2, 3, 500);
        assert!(b.can_replan());
        b.record_replan();
        b.record_replan();
        assert!(!b.can_replan());
    }

    #[test]
    fn repair_attempts_decrement() {
        let mut b = BudgetManager::new(1000, 1, 3, 500);
        assert!(b.can_repair());
        b.record_repair();
        b.record_repair();
        b.record_repair();
        assert!(!b.can_repair());
    }

    #[test]
    fn neural_budget_rejects_when_class_exceeded() {
        use disyn_core::types::CostClass;
        let mut b = BudgetManager::new(10_000, 1, 3, 100);
        b.record(&ResourceUsage {
            total_tokens: 90,
            symbolic_tokens: 0,
            neural_tokens: 90,
            wall_time_ms: 10,
        });
        assert!(!b.can_afford_neural(&CostEstimate {
            class: Some(CostClass::Neural),
            input_tokens: 50,
            output_tokens: 50,
        }));
    }
}