use serde::{Deserialize, Serialize};
use crate::economic::{EconomicMode, ModelTier};
use crate::gating::HomeostaticState;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GatingDecision {
pub rule_id: String,
pub economic_mode: Option<EconomicMode>,
pub max_tokens_next_turn: Option<u32>,
pub preferred_model: Option<ModelTier>,
pub restrict_expensive_tools: Option<bool>,
pub restrict_side_effects: Option<bool>,
pub max_tool_calls_per_tick: Option<u32>,
pub rationale: String,
}
impl GatingDecision {
pub fn noop(rule_id: impl Into<String>) -> Self {
Self {
rule_id: rule_id.into(),
economic_mode: None,
max_tokens_next_turn: None,
preferred_model: None,
restrict_expensive_tools: None,
restrict_side_effects: None,
max_tool_calls_per_tick: None,
rationale: String::new(),
}
}
}
pub trait HomeostaticRule: Send + Sync {
fn rule_id(&self) -> &str;
fn evaluate(&self, state: &HomeostaticState) -> Option<GatingDecision>;
}
pub struct RuleSet {
rules: Vec<Box<dyn HomeostaticRule>>,
}
impl RuleSet {
pub fn new() -> Self {
Self { rules: Vec::new() }
}
pub fn add(&mut self, rule: Box<dyn HomeostaticRule>) {
self.rules.push(rule);
}
pub fn evaluate_all(&self, state: &HomeostaticState) -> Vec<GatingDecision> {
self.rules
.iter()
.filter_map(|rule| rule.evaluate(state))
.collect()
}
pub fn len(&self) -> usize {
self.rules.len()
}
pub fn is_empty(&self) -> bool {
self.rules.is_empty()
}
}
impl Default for RuleSet {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::gating::HomeostaticState;
struct AlwaysFireRule;
impl HomeostaticRule for AlwaysFireRule {
fn rule_id(&self) -> &str {
"always_fire"
}
fn evaluate(&self, _state: &HomeostaticState) -> Option<GatingDecision> {
Some(GatingDecision {
rule_id: self.rule_id().into(),
economic_mode: Some(EconomicMode::Conserving),
rationale: "always fires".into(),
..GatingDecision::noop(self.rule_id())
})
}
}
struct NeverFireRule;
impl HomeostaticRule for NeverFireRule {
fn rule_id(&self) -> &str {
"never_fire"
}
fn evaluate(&self, _state: &HomeostaticState) -> Option<GatingDecision> {
None
}
}
#[test]
fn rule_set_evaluates_all() {
let mut set = RuleSet::new();
set.add(Box::new(AlwaysFireRule));
set.add(Box::new(NeverFireRule));
set.add(Box::new(AlwaysFireRule));
let state = HomeostaticState::default();
let decisions = set.evaluate_all(&state);
assert_eq!(decisions.len(), 2);
}
#[test]
fn rule_set_empty() {
let set = RuleSet::new();
assert!(set.is_empty());
assert_eq!(set.len(), 0);
}
#[test]
fn gating_decision_noop() {
let decision = GatingDecision::noop("test");
assert_eq!(decision.rule_id, "test");
assert!(decision.economic_mode.is_none());
assert!(decision.max_tokens_next_turn.is_none());
}
}