autonomic_core/
economic.rs1use serde::{Deserialize, Serialize};
7
8use crate::hysteresis::HysteresisGate;
9
10#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
14#[serde(rename_all = "snake_case")]
15pub enum EconomicMode {
16 #[default]
18 Sovereign,
19 Conserving,
21 Hustle,
23 Hibernate,
25}
26
27#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
29#[serde(rename_all = "snake_case")]
30pub enum ModelTier {
31 Flagship,
33 #[default]
35 Standard,
36 Budget,
38}
39
40#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct EconomicState {
43 pub identity_address: Option<String>,
45 pub balance_micro_credits: i64,
47 pub lifetime_revenue: i64,
49 pub lifetime_costs: i64,
51 pub monthly_burn_estimate: i64,
53 pub mode: EconomicMode,
55 pub cost_last_5min: i64,
57 pub last_cost_event_ms: u64,
59 pub mode_gate: HysteresisGate,
61}
62
63impl Default for EconomicState {
64 fn default() -> Self {
65 Self {
66 identity_address: None,
67 balance_micro_credits: 10_000_000, lifetime_revenue: 0,
69 lifetime_costs: 0,
70 monthly_burn_estimate: 0,
71 mode: EconomicMode::Sovereign,
72 cost_last_5min: 0,
73 last_cost_event_ms: 0,
74 mode_gate: HysteresisGate::new(0.7, 0.3, 30_000),
77 }
78 }
79}
80
81impl EconomicState {
82 pub fn balance_to_burn_ratio(&self) -> f64 {
84 if self.monthly_burn_estimate <= 0 {
85 return f64::INFINITY;
86 }
87 self.balance_micro_credits as f64 / self.monthly_burn_estimate as f64
88 }
89}
90
91#[derive(Debug, Clone, Serialize, Deserialize)]
93pub struct ModelCostRates {
94 pub input_per_token: i64,
95 pub output_per_token: i64,
96}
97
98impl Default for ModelCostRates {
99 fn default() -> Self {
100 Self {
102 input_per_token: 3, output_per_token: 15, }
105 }
106}
107
108#[derive(Debug, Clone, Serialize, Deserialize)]
110#[serde(rename_all = "snake_case")]
111pub enum CostReason {
112 ModelInference {
114 model: String,
115 prompt_tokens: u32,
116 completion_tokens: u32,
117 },
118 ToolExecution { tool_name: String },
120 Storage { bytes: u64 },
122 Adjustment { description: String },
124}
125
126#[cfg(test)]
127mod tests {
128 use super::*;
129
130 #[test]
131 fn economic_mode_serde_roundtrip() {
132 for mode in [
133 EconomicMode::Sovereign,
134 EconomicMode::Conserving,
135 EconomicMode::Hustle,
136 EconomicMode::Hibernate,
137 ] {
138 let json = serde_json::to_string(&mode).unwrap();
139 let back: EconomicMode = serde_json::from_str(&json).unwrap();
140 assert_eq!(mode, back);
141 }
142 }
143
144 #[test]
145 fn model_tier_serde_roundtrip() {
146 for tier in [ModelTier::Flagship, ModelTier::Standard, ModelTier::Budget] {
147 let json = serde_json::to_string(&tier).unwrap();
148 let back: ModelTier = serde_json::from_str(&json).unwrap();
149 assert_eq!(tier, back);
150 }
151 }
152
153 #[test]
154 fn economic_state_default() {
155 let state = EconomicState::default();
156 assert_eq!(state.balance_micro_credits, 10_000_000);
157 assert_eq!(state.mode, EconomicMode::Sovereign);
158 assert_eq!(state.lifetime_costs, 0);
159 }
160
161 #[test]
162 fn balance_to_burn_ratio_zero_burn() {
163 let state = EconomicState::default();
164 assert!(state.balance_to_burn_ratio().is_infinite());
165 }
166
167 #[test]
168 fn balance_to_burn_ratio_normal() {
169 let state = EconomicState {
170 balance_micro_credits: 2_000_000,
171 monthly_burn_estimate: 1_000_000,
172 ..Default::default()
173 };
174 assert!((state.balance_to_burn_ratio() - 2.0).abs() < f64::EPSILON);
175 }
176
177 #[test]
178 fn cost_reason_serde_roundtrip() {
179 let reason = CostReason::ModelInference {
180 model: "claude-sonnet".into(),
181 prompt_tokens: 100,
182 completion_tokens: 50,
183 };
184 let json = serde_json::to_string(&reason).unwrap();
185 let back: CostReason = serde_json::from_str(&json).unwrap();
186 assert!(matches!(back, CostReason::ModelInference { .. }));
187 }
188}