autonomic-core 0.3.0

Core types, traits, and errors for the Autonomic homeostasis controller
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
//! Gating profiles and homeostatic state.
//!
//! `AutonomicGatingProfile` extends the canonical `GatingProfile` with
//! economic regulation. The three-pillar `HomeostaticState` captures
//! operational, cognitive, and economic health.

use aios_protocol::mode::{GatingProfile, OperatingMode};
use serde::{Deserialize, Serialize};

use crate::economic::{EconomicMode, EconomicState, ModelTier};
use crate::hysteresis::HysteresisGate;

/// Economic gates — extensions to the canonical `GatingProfile`.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EconomicGates {
    /// Current economic operating mode.
    pub economic_mode: EconomicMode,
    /// Maximum tokens allowed for the next turn (advisory).
    pub max_tokens_next_turn: Option<u32>,
    /// Preferred model tier for cost control.
    pub preferred_model: Option<ModelTier>,
    /// Whether expensive tools (e.g., web search, code execution) are allowed.
    pub allow_expensive_tools: bool,
    /// Whether agent replication is allowed.
    pub allow_replication: bool,
}

impl Default for EconomicGates {
    fn default() -> Self {
        Self {
            economic_mode: EconomicMode::Sovereign,
            max_tokens_next_turn: None,
            preferred_model: None,
            allow_expensive_tools: true,
            allow_replication: true,
        }
    }
}

/// The full gating profile emitted by the Autonomic controller.
///
/// Embeds the canonical `GatingProfile` for operational gates and adds
/// economic regulation on top.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct AutonomicGatingProfile {
    /// Canonical operational gates (from aios-protocol).
    pub operational: GatingProfile,
    /// Economic regulation gates (Autonomic extension).
    pub economic: EconomicGates,
    /// Human-readable rationale for why this profile was chosen.
    pub rationale: Vec<String>,
}

/// Operational health state — derived from `AgentStateVector` events.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OperationalState {
    /// Current operating mode.
    pub mode: OperatingMode,
    /// Consecutive error count.
    pub error_streak: u32,
    /// Total errors seen.
    pub total_errors: u32,
    /// Total successful actions.
    pub total_successes: u32,
    /// Timestamp of last tick (ms since epoch).
    pub last_tick_ms: u64,
}

impl Default for OperationalState {
    fn default() -> Self {
        Self {
            mode: OperatingMode::Execute,
            error_streak: 0,
            total_errors: 0,
            total_successes: 0,
            last_tick_ms: 0,
        }
    }
}

/// Cognitive health state — tracks context and token usage.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CognitiveState {
    /// Total tokens consumed in the session.
    pub total_tokens_used: u64,
    /// Tokens remaining from budget.
    pub tokens_remaining: u64,
    /// Context pressure (0.0 = empty, 1.0 = full).
    pub context_pressure: f32,
    /// Number of model turns completed.
    pub turns_completed: u32,
    /// Average tool calls per turn (rolling window). High = active implementation.
    pub tool_density: f64,
    /// Turns elapsed since last compaction. High = stale old context.
    pub turns_since_compact: u32,
    /// Hysteresis gate for context dilation — prevents flapping between
    /// Dilate and Compress decisions in the soft zone.
    pub dilation_gate: HysteresisGate,
}

impl Default for CognitiveState {
    fn default() -> Self {
        Self {
            total_tokens_used: 0,
            tokens_remaining: 120_000,
            context_pressure: 0.0,
            turns_completed: 0,
            tool_density: 0.0,
            turns_since_compact: 0,
            // Dilation gate: enters dilation at 60% pressure, exits at 45%.
            // min_hold_ms=0 because the shell tracks turns, not wall-clock time.
            dilation_gate: HysteresisGate::new(0.60, 0.45, 0),
        }
    }
}

/// Strategy event tracking state.
///
/// Accumulated from `strategy.*` custom events emitted by strategy skills
/// to Lago. Used by advisory rules to inform risk assessment and suggest
/// setpoint reviews.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct StrategyState {
    /// Count of drift-check alerts received.
    pub drift_alerts: u32,
    /// Count of decisions logged.
    pub decisions_logged: u32,
    /// Count of strategy critiques completed.
    pub critiques_completed: u32,
    /// Timestamp of the most recent strategy event (ms since epoch).
    pub last_strategy_event_ms: u64,
}

/// Evaluation quality tracking state.
///
/// Accumulated from `eval.*` custom events emitted by Nous evaluators.
/// Used by the `EvalQualityRule` to gate agent behavior based on quality scores.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EvalState {
    /// Count of inline evaluations completed.
    pub inline_eval_count: u32,
    /// Count of async evaluations completed.
    pub async_eval_count: u32,
    /// Aggregate quality score (0.0..1.0), exponential moving average.
    pub aggregate_quality_score: f64,
    /// Quality trend (positive = improving, negative = degrading).
    pub quality_trend: f64,
    /// Timestamp of the last evaluation (ms since epoch).
    pub last_eval_ms: u64,
}

impl Default for EvalState {
    fn default() -> Self {
        Self {
            inline_eval_count: 0,
            async_eval_count: 0,
            aggregate_quality_score: 1.0, // Optimistic start
            quality_trend: 0.0,
            last_eval_ms: 0,
        }
    }
}

/// Belief state — tracks Anima agent belief metrics.
///
/// Accumulated from `anima.*` custom events emitted to Lago.
/// Provides the Autonomic controller with visibility into the
/// agent's capability set, trust network, and policy compliance.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BeliefState {
    /// Number of currently granted capabilities.
    pub capability_count: u32,
    /// Number of peers with trust scores.
    pub trust_peer_count: u32,
    /// Average trust score across all peers (0.0..1.0).
    pub average_trust: f64,
    /// Minimum trust score across all peers (0.0..1.0).
    pub min_trust: f64,
    /// Overall reputation score (0.0..1.0).
    pub reputation_score: f64,
    /// Number of policy violations detected.
    pub violations: u64,
    /// Timestamp of the last belief-related event (ms since epoch).
    pub last_belief_event_ms: u64,
}

impl Default for BeliefState {
    fn default() -> Self {
        Self {
            capability_count: 0,
            trust_peer_count: 0,
            average_trust: 1.0, // Optimistic start (no peers = full trust)
            min_trust: 1.0,
            reputation_score: 1.0, // Optimistic start
            violations: 0,
            last_belief_event_ms: 0,
        }
    }
}

/// The homeostatic state for an agent session.
///
/// This is the projection state: accumulated from the event stream
/// and used as input to the rule engine.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct HomeostaticState {
    /// Agent/session identifier.
    pub agent_id: String,
    /// Operational health.
    pub operational: OperationalState,
    /// Cognitive health.
    pub cognitive: CognitiveState,
    /// Economic health.
    pub economic: EconomicState,
    /// Strategy event tracking.
    pub strategy: StrategyState,
    /// Evaluation quality tracking.
    pub eval: EvalState,
    /// Anima belief tracking.
    pub belief: BeliefState,
    /// Sequence number of the last event processed.
    pub last_event_seq: u64,
    /// Timestamp of the last event processed (ms since epoch).
    pub last_event_ms: u64,
}

impl HomeostaticState {
    /// Create a new state for the given agent.
    pub fn for_agent(agent_id: impl Into<String>) -> Self {
        Self {
            agent_id: agent_id.into(),
            ..Default::default()
        }
    }
}

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

    #[test]
    fn autonomic_gating_profile_default() {
        let profile = AutonomicGatingProfile::default();
        assert!(profile.operational.allow_side_effects);
        assert!(profile.economic.allow_expensive_tools);
        assert_eq!(profile.economic.economic_mode, EconomicMode::Sovereign);
        assert!(profile.rationale.is_empty());
    }

    #[test]
    fn autonomic_gating_profile_serde_roundtrip() {
        let profile = AutonomicGatingProfile {
            operational: GatingProfile::default(),
            economic: EconomicGates {
                economic_mode: EconomicMode::Conserving,
                max_tokens_next_turn: Some(4096),
                preferred_model: Some(ModelTier::Budget),
                allow_expensive_tools: false,
                allow_replication: false,
            },
            rationale: vec!["balance low".into(), "reducing spend".into()],
        };
        let json = serde_json::to_string(&profile).unwrap();
        let back: AutonomicGatingProfile = serde_json::from_str(&json).unwrap();
        assert_eq!(back.economic.economic_mode, EconomicMode::Conserving);
        assert_eq!(back.economic.max_tokens_next_turn, Some(4096));
        assert!(!back.economic.allow_expensive_tools);
        assert_eq!(back.rationale.len(), 2);
    }

    #[test]
    fn homeostatic_state_for_agent() {
        let state = HomeostaticState::for_agent("agent-1");
        assert_eq!(state.agent_id, "agent-1");
        assert_eq!(state.operational.mode, OperatingMode::Execute);
        assert_eq!(state.economic.mode, EconomicMode::Sovereign);
    }

    #[test]
    fn strategy_state_default_is_zeroed() {
        let strategy = StrategyState::default();
        assert_eq!(strategy.drift_alerts, 0);
        assert_eq!(strategy.decisions_logged, 0);
        assert_eq!(strategy.critiques_completed, 0);
        assert_eq!(strategy.last_strategy_event_ms, 0);
    }

    #[test]
    fn homeostatic_state_includes_strategy() {
        let state = HomeostaticState::for_agent("agent-1");
        assert_eq!(state.strategy.drift_alerts, 0);
        assert_eq!(state.strategy.decisions_logged, 0);
        assert_eq!(state.strategy.critiques_completed, 0);
        assert_eq!(state.strategy.last_strategy_event_ms, 0);
    }

    #[test]
    fn strategy_state_serde_roundtrip() {
        let strategy = StrategyState {
            drift_alerts: 5,
            decisions_logged: 12,
            critiques_completed: 3,
            last_strategy_event_ms: 1_700_000_000_000,
        };
        let json = serde_json::to_string(&strategy).unwrap();
        let back: StrategyState = serde_json::from_str(&json).unwrap();
        assert_eq!(back.drift_alerts, 5);
        assert_eq!(back.decisions_logged, 12);
        assert_eq!(back.critiques_completed, 3);
        assert_eq!(back.last_strategy_event_ms, 1_700_000_000_000);
    }

    #[test]
    fn eval_state_default_optimistic() {
        let eval = EvalState::default();
        assert_eq!(eval.inline_eval_count, 0);
        assert_eq!(eval.async_eval_count, 0);
        assert!((eval.aggregate_quality_score - 1.0).abs() < f64::EPSILON);
        assert!((eval.quality_trend).abs() < f64::EPSILON);
        assert_eq!(eval.last_eval_ms, 0);
    }

    #[test]
    fn eval_state_serde_roundtrip() {
        let eval = EvalState {
            inline_eval_count: 15,
            async_eval_count: 3,
            aggregate_quality_score: 0.78,
            quality_trend: -0.02,
            last_eval_ms: 1_700_000_000_000,
        };
        let json = serde_json::to_string(&eval).unwrap();
        let back: EvalState = serde_json::from_str(&json).unwrap();
        assert_eq!(back.inline_eval_count, 15);
        assert_eq!(back.async_eval_count, 3);
        assert!((back.aggregate_quality_score - 0.78).abs() < f64::EPSILON);
        assert!((back.quality_trend - (-0.02)).abs() < f64::EPSILON);
    }

    #[test]
    fn homeostatic_state_includes_eval() {
        let state = HomeostaticState::for_agent("test");
        assert_eq!(state.eval.inline_eval_count, 0);
        assert!((state.eval.aggregate_quality_score - 1.0).abs() < f64::EPSILON);
    }

    #[test]
    fn belief_state_default_optimistic() {
        let belief = BeliefState::default();
        assert_eq!(belief.capability_count, 0);
        assert_eq!(belief.trust_peer_count, 0);
        assert!((belief.average_trust - 1.0).abs() < f64::EPSILON);
        assert!((belief.min_trust - 1.0).abs() < f64::EPSILON);
        assert!((belief.reputation_score - 1.0).abs() < f64::EPSILON);
        assert_eq!(belief.violations, 0);
        assert_eq!(belief.last_belief_event_ms, 0);
    }

    #[test]
    fn belief_state_serde_roundtrip() {
        let belief = BeliefState {
            capability_count: 5,
            trust_peer_count: 3,
            average_trust: 0.72,
            min_trust: 0.45,
            reputation_score: 0.88,
            violations: 2,
            last_belief_event_ms: 1_700_000_000_000,
        };
        let json = serde_json::to_string(&belief).unwrap();
        let back: BeliefState = serde_json::from_str(&json).unwrap();
        assert_eq!(back.capability_count, 5);
        assert_eq!(back.trust_peer_count, 3);
        assert!((back.average_trust - 0.72).abs() < f64::EPSILON);
        assert!((back.min_trust - 0.45).abs() < f64::EPSILON);
        assert!((back.reputation_score - 0.88).abs() < f64::EPSILON);
        assert_eq!(back.violations, 2);
    }

    #[test]
    fn homeostatic_state_includes_belief() {
        let state = HomeostaticState::for_agent("test");
        assert_eq!(state.belief.capability_count, 0);
        assert_eq!(state.belief.violations, 0);
        assert!((state.belief.reputation_score - 1.0).abs() < f64::EPSILON);
    }

    #[test]
    fn cognitive_state_has_dilation_gate() {
        let cog = CognitiveState::default();
        assert!(!cog.dilation_gate.active);
        assert!((cog.dilation_gate.threshold_enter - 0.60).abs() < f64::EPSILON);
        assert!((cog.dilation_gate.threshold_exit - 0.45).abs() < f64::EPSILON);
    }

    #[test]
    fn cognitive_state_has_compression_signals() {
        let mut cog = CognitiveState::default();
        assert_eq!(cog.tool_density, 0.0);
        assert_eq!(cog.turns_since_compact, 0);
        cog.tool_density = 3.5;
        cog.turns_since_compact = 12;
        assert!((cog.tool_density - 3.5).abs() < f64::EPSILON);
        assert_eq!(cog.turns_since_compact, 12);
    }

    #[test]
    fn cognitive_state_compression_signals_serde() {
        let cog = CognitiveState {
            tool_density: 2.5,
            turns_since_compact: 8,
            ..Default::default()
        };
        let json = serde_json::to_string(&cog).unwrap();
        let back: CognitiveState = serde_json::from_str(&json).unwrap();
        assert!((back.tool_density - 2.5).abs() < f64::EPSILON);
        assert_eq!(back.turns_since_compact, 8);
    }
}