Skip to main content

autonomic_core/
gating.rs

1//! Gating profiles and homeostatic state.
2//!
3//! `AutonomicGatingProfile` extends the canonical `GatingProfile` with
4//! economic regulation. The three-pillar `HomeostaticState` captures
5//! operational, cognitive, and economic health.
6
7use aios_protocol::mode::{GatingProfile, OperatingMode};
8use serde::{Deserialize, Serialize};
9
10use crate::economic::{EconomicMode, EconomicState, ModelTier};
11use crate::hysteresis::HysteresisGate;
12
13/// Economic gates — extensions to the canonical `GatingProfile`.
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct EconomicGates {
16    /// Current economic operating mode.
17    pub economic_mode: EconomicMode,
18    /// Maximum tokens allowed for the next turn (advisory).
19    pub max_tokens_next_turn: Option<u32>,
20    /// Preferred model tier for cost control.
21    pub preferred_model: Option<ModelTier>,
22    /// Whether expensive tools (e.g., web search, code execution) are allowed.
23    pub allow_expensive_tools: bool,
24    /// Whether agent replication is allowed.
25    pub allow_replication: bool,
26}
27
28impl Default for EconomicGates {
29    fn default() -> Self {
30        Self {
31            economic_mode: EconomicMode::Sovereign,
32            max_tokens_next_turn: None,
33            preferred_model: None,
34            allow_expensive_tools: true,
35            allow_replication: true,
36        }
37    }
38}
39
40/// The full gating profile emitted by the Autonomic controller.
41///
42/// Embeds the canonical `GatingProfile` for operational gates and adds
43/// economic regulation on top.
44#[derive(Debug, Clone, Default, Serialize, Deserialize)]
45pub struct AutonomicGatingProfile {
46    /// Canonical operational gates (from aios-protocol).
47    pub operational: GatingProfile,
48    /// Economic regulation gates (Autonomic extension).
49    pub economic: EconomicGates,
50    /// Human-readable rationale for why this profile was chosen.
51    pub rationale: Vec<String>,
52}
53
54/// Operational health state — derived from `AgentStateVector` events.
55#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct OperationalState {
57    /// Current operating mode.
58    pub mode: OperatingMode,
59    /// Consecutive error count.
60    pub error_streak: u32,
61    /// Total errors seen.
62    pub total_errors: u32,
63    /// Total successful actions.
64    pub total_successes: u32,
65    /// Timestamp of last tick (ms since epoch).
66    pub last_tick_ms: u64,
67}
68
69impl Default for OperationalState {
70    fn default() -> Self {
71        Self {
72            mode: OperatingMode::Execute,
73            error_streak: 0,
74            total_errors: 0,
75            total_successes: 0,
76            last_tick_ms: 0,
77        }
78    }
79}
80
81/// Cognitive health state — tracks context and token usage.
82#[derive(Debug, Clone, Serialize, Deserialize)]
83pub struct CognitiveState {
84    /// Total tokens consumed in the session.
85    pub total_tokens_used: u64,
86    /// Tokens remaining from budget.
87    pub tokens_remaining: u64,
88    /// Context pressure (0.0 = empty, 1.0 = full).
89    pub context_pressure: f32,
90    /// Number of model turns completed.
91    pub turns_completed: u32,
92    /// Average tool calls per turn (rolling window). High = active implementation.
93    pub tool_density: f64,
94    /// Turns elapsed since last compaction. High = stale old context.
95    pub turns_since_compact: u32,
96    /// Hysteresis gate for context dilation — prevents flapping between
97    /// Dilate and Compress decisions in the soft zone.
98    pub dilation_gate: HysteresisGate,
99}
100
101impl Default for CognitiveState {
102    fn default() -> Self {
103        Self {
104            total_tokens_used: 0,
105            tokens_remaining: 120_000,
106            context_pressure: 0.0,
107            turns_completed: 0,
108            tool_density: 0.0,
109            turns_since_compact: 0,
110            // Dilation gate: enters dilation at 60% pressure, exits at 45%.
111            // min_hold_ms=0 because the shell tracks turns, not wall-clock time.
112            dilation_gate: HysteresisGate::new(0.60, 0.45, 0),
113        }
114    }
115}
116
117/// Strategy event tracking state.
118///
119/// Accumulated from `strategy.*` custom events emitted by strategy skills
120/// to Lago. Used by advisory rules to inform risk assessment and suggest
121/// setpoint reviews.
122#[derive(Debug, Clone, Default, Serialize, Deserialize)]
123pub struct StrategyState {
124    /// Count of drift-check alerts received.
125    pub drift_alerts: u32,
126    /// Count of decisions logged.
127    pub decisions_logged: u32,
128    /// Count of strategy critiques completed.
129    pub critiques_completed: u32,
130    /// Timestamp of the most recent strategy event (ms since epoch).
131    pub last_strategy_event_ms: u64,
132}
133
134/// Evaluation quality tracking state.
135///
136/// Accumulated from `eval.*` custom events emitted by Nous evaluators.
137/// Used by the `EvalQualityRule` to gate agent behavior based on quality scores.
138#[derive(Debug, Clone, Serialize, Deserialize)]
139pub struct EvalState {
140    /// Count of inline evaluations completed.
141    pub inline_eval_count: u32,
142    /// Count of async evaluations completed.
143    pub async_eval_count: u32,
144    /// Aggregate quality score (0.0..1.0), exponential moving average.
145    pub aggregate_quality_score: f64,
146    /// Quality trend (positive = improving, negative = degrading).
147    pub quality_trend: f64,
148    /// Timestamp of the last evaluation (ms since epoch).
149    pub last_eval_ms: u64,
150}
151
152impl Default for EvalState {
153    fn default() -> Self {
154        Self {
155            inline_eval_count: 0,
156            async_eval_count: 0,
157            aggregate_quality_score: 1.0, // Optimistic start
158            quality_trend: 0.0,
159            last_eval_ms: 0,
160        }
161    }
162}
163
164/// Belief state — tracks Anima agent belief metrics.
165///
166/// Accumulated from `anima.*` custom events emitted to Lago.
167/// Provides the Autonomic controller with visibility into the
168/// agent's capability set, trust network, and policy compliance.
169#[derive(Debug, Clone, Serialize, Deserialize)]
170pub struct BeliefState {
171    /// Number of currently granted capabilities.
172    pub capability_count: u32,
173    /// Number of peers with trust scores.
174    pub trust_peer_count: u32,
175    /// Average trust score across all peers (0.0..1.0).
176    pub average_trust: f64,
177    /// Minimum trust score across all peers (0.0..1.0).
178    pub min_trust: f64,
179    /// Overall reputation score (0.0..1.0).
180    pub reputation_score: f64,
181    /// Number of policy violations detected.
182    pub violations: u64,
183    /// Timestamp of the last belief-related event (ms since epoch).
184    pub last_belief_event_ms: u64,
185}
186
187impl Default for BeliefState {
188    fn default() -> Self {
189        Self {
190            capability_count: 0,
191            trust_peer_count: 0,
192            average_trust: 1.0, // Optimistic start (no peers = full trust)
193            min_trust: 1.0,
194            reputation_score: 1.0, // Optimistic start
195            violations: 0,
196            last_belief_event_ms: 0,
197        }
198    }
199}
200
201/// The homeostatic state for an agent session.
202///
203/// This is the projection state: accumulated from the event stream
204/// and used as input to the rule engine.
205#[derive(Debug, Clone, Default, Serialize, Deserialize)]
206pub struct HomeostaticState {
207    /// Agent/session identifier.
208    pub agent_id: String,
209    /// Operational health.
210    pub operational: OperationalState,
211    /// Cognitive health.
212    pub cognitive: CognitiveState,
213    /// Economic health.
214    pub economic: EconomicState,
215    /// Strategy event tracking.
216    pub strategy: StrategyState,
217    /// Evaluation quality tracking.
218    pub eval: EvalState,
219    /// Anima belief tracking.
220    pub belief: BeliefState,
221    /// Sequence number of the last event processed.
222    pub last_event_seq: u64,
223    /// Timestamp of the last event processed (ms since epoch).
224    pub last_event_ms: u64,
225}
226
227impl HomeostaticState {
228    /// Create a new state for the given agent.
229    pub fn for_agent(agent_id: impl Into<String>) -> Self {
230        Self {
231            agent_id: agent_id.into(),
232            ..Default::default()
233        }
234    }
235}
236
237#[cfg(test)]
238mod tests {
239    use super::*;
240
241    #[test]
242    fn autonomic_gating_profile_default() {
243        let profile = AutonomicGatingProfile::default();
244        assert!(profile.operational.allow_side_effects);
245        assert!(profile.economic.allow_expensive_tools);
246        assert_eq!(profile.economic.economic_mode, EconomicMode::Sovereign);
247        assert!(profile.rationale.is_empty());
248    }
249
250    #[test]
251    fn autonomic_gating_profile_serde_roundtrip() {
252        let profile = AutonomicGatingProfile {
253            operational: GatingProfile::default(),
254            economic: EconomicGates {
255                economic_mode: EconomicMode::Conserving,
256                max_tokens_next_turn: Some(4096),
257                preferred_model: Some(ModelTier::Budget),
258                allow_expensive_tools: false,
259                allow_replication: false,
260            },
261            rationale: vec!["balance low".into(), "reducing spend".into()],
262        };
263        let json = serde_json::to_string(&profile).unwrap();
264        let back: AutonomicGatingProfile = serde_json::from_str(&json).unwrap();
265        assert_eq!(back.economic.economic_mode, EconomicMode::Conserving);
266        assert_eq!(back.economic.max_tokens_next_turn, Some(4096));
267        assert!(!back.economic.allow_expensive_tools);
268        assert_eq!(back.rationale.len(), 2);
269    }
270
271    #[test]
272    fn homeostatic_state_for_agent() {
273        let state = HomeostaticState::for_agent("agent-1");
274        assert_eq!(state.agent_id, "agent-1");
275        assert_eq!(state.operational.mode, OperatingMode::Execute);
276        assert_eq!(state.economic.mode, EconomicMode::Sovereign);
277    }
278
279    #[test]
280    fn strategy_state_default_is_zeroed() {
281        let strategy = StrategyState::default();
282        assert_eq!(strategy.drift_alerts, 0);
283        assert_eq!(strategy.decisions_logged, 0);
284        assert_eq!(strategy.critiques_completed, 0);
285        assert_eq!(strategy.last_strategy_event_ms, 0);
286    }
287
288    #[test]
289    fn homeostatic_state_includes_strategy() {
290        let state = HomeostaticState::for_agent("agent-1");
291        assert_eq!(state.strategy.drift_alerts, 0);
292        assert_eq!(state.strategy.decisions_logged, 0);
293        assert_eq!(state.strategy.critiques_completed, 0);
294        assert_eq!(state.strategy.last_strategy_event_ms, 0);
295    }
296
297    #[test]
298    fn strategy_state_serde_roundtrip() {
299        let strategy = StrategyState {
300            drift_alerts: 5,
301            decisions_logged: 12,
302            critiques_completed: 3,
303            last_strategy_event_ms: 1_700_000_000_000,
304        };
305        let json = serde_json::to_string(&strategy).unwrap();
306        let back: StrategyState = serde_json::from_str(&json).unwrap();
307        assert_eq!(back.drift_alerts, 5);
308        assert_eq!(back.decisions_logged, 12);
309        assert_eq!(back.critiques_completed, 3);
310        assert_eq!(back.last_strategy_event_ms, 1_700_000_000_000);
311    }
312
313    #[test]
314    fn eval_state_default_optimistic() {
315        let eval = EvalState::default();
316        assert_eq!(eval.inline_eval_count, 0);
317        assert_eq!(eval.async_eval_count, 0);
318        assert!((eval.aggregate_quality_score - 1.0).abs() < f64::EPSILON);
319        assert!((eval.quality_trend).abs() < f64::EPSILON);
320        assert_eq!(eval.last_eval_ms, 0);
321    }
322
323    #[test]
324    fn eval_state_serde_roundtrip() {
325        let eval = EvalState {
326            inline_eval_count: 15,
327            async_eval_count: 3,
328            aggregate_quality_score: 0.78,
329            quality_trend: -0.02,
330            last_eval_ms: 1_700_000_000_000,
331        };
332        let json = serde_json::to_string(&eval).unwrap();
333        let back: EvalState = serde_json::from_str(&json).unwrap();
334        assert_eq!(back.inline_eval_count, 15);
335        assert_eq!(back.async_eval_count, 3);
336        assert!((back.aggregate_quality_score - 0.78).abs() < f64::EPSILON);
337        assert!((back.quality_trend - (-0.02)).abs() < f64::EPSILON);
338    }
339
340    #[test]
341    fn homeostatic_state_includes_eval() {
342        let state = HomeostaticState::for_agent("test");
343        assert_eq!(state.eval.inline_eval_count, 0);
344        assert!((state.eval.aggregate_quality_score - 1.0).abs() < f64::EPSILON);
345    }
346
347    #[test]
348    fn belief_state_default_optimistic() {
349        let belief = BeliefState::default();
350        assert_eq!(belief.capability_count, 0);
351        assert_eq!(belief.trust_peer_count, 0);
352        assert!((belief.average_trust - 1.0).abs() < f64::EPSILON);
353        assert!((belief.min_trust - 1.0).abs() < f64::EPSILON);
354        assert!((belief.reputation_score - 1.0).abs() < f64::EPSILON);
355        assert_eq!(belief.violations, 0);
356        assert_eq!(belief.last_belief_event_ms, 0);
357    }
358
359    #[test]
360    fn belief_state_serde_roundtrip() {
361        let belief = BeliefState {
362            capability_count: 5,
363            trust_peer_count: 3,
364            average_trust: 0.72,
365            min_trust: 0.45,
366            reputation_score: 0.88,
367            violations: 2,
368            last_belief_event_ms: 1_700_000_000_000,
369        };
370        let json = serde_json::to_string(&belief).unwrap();
371        let back: BeliefState = serde_json::from_str(&json).unwrap();
372        assert_eq!(back.capability_count, 5);
373        assert_eq!(back.trust_peer_count, 3);
374        assert!((back.average_trust - 0.72).abs() < f64::EPSILON);
375        assert!((back.min_trust - 0.45).abs() < f64::EPSILON);
376        assert!((back.reputation_score - 0.88).abs() < f64::EPSILON);
377        assert_eq!(back.violations, 2);
378    }
379
380    #[test]
381    fn homeostatic_state_includes_belief() {
382        let state = HomeostaticState::for_agent("test");
383        assert_eq!(state.belief.capability_count, 0);
384        assert_eq!(state.belief.violations, 0);
385        assert!((state.belief.reputation_score - 1.0).abs() < f64::EPSILON);
386    }
387
388    #[test]
389    fn cognitive_state_has_dilation_gate() {
390        let cog = CognitiveState::default();
391        assert!(!cog.dilation_gate.active);
392        assert!((cog.dilation_gate.threshold_enter - 0.60).abs() < f64::EPSILON);
393        assert!((cog.dilation_gate.threshold_exit - 0.45).abs() < f64::EPSILON);
394    }
395
396    #[test]
397    fn cognitive_state_has_compression_signals() {
398        let mut cog = CognitiveState::default();
399        assert_eq!(cog.tool_density, 0.0);
400        assert_eq!(cog.turns_since_compact, 0);
401        cog.tool_density = 3.5;
402        cog.turns_since_compact = 12;
403        assert!((cog.tool_density - 3.5).abs() < f64::EPSILON);
404        assert_eq!(cog.turns_since_compact, 12);
405    }
406
407    #[test]
408    fn cognitive_state_compression_signals_serde() {
409        let cog = CognitiveState {
410            tool_density: 2.5,
411            turns_since_compact: 8,
412            ..Default::default()
413        };
414        let json = serde_json::to_string(&cog).unwrap();
415        let back: CognitiveState = serde_json::from_str(&json).unwrap();
416        assert!((back.tool_density - 2.5).abs() < f64::EPSILON);
417        assert_eq!(back.turns_since_compact, 8);
418    }
419}