Skip to main content

zagens_core/
coherence.rs

1//! Plain-language session coherence state + capacity-event reducer
2//! (P2 PR4 enum entry; M1-deferred reducer landed in M6 2026-05-25).
3//!
4//! `CoherenceState` (the user-facing ladder enum) has lived here since
5//! P2 PR4. M1 (spike row #22) was originally going to bring the
6//! reducer (`CoherenceSignal` + `next_coherence_state`) along, but
7//! the reducer depends on `GuardrailAction` / `RiskBand` from the
8//! `capacity` module — those only landed in core under M6, so M1
9//! deferred this part. M6 now closes that loop: the reducer joins
10//! the state enum here, sharing local `super::capacity::{...}`
11//! references instead of crossing a crate boundary.
12
13use schemars::JsonSchema;
14use serde::{Deserialize, Serialize};
15
16use crate::capacity::{GuardrailAction, RiskBand};
17
18/// User-facing coherence ladder for session health.
19#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
20#[serde(rename_all = "snake_case")]
21pub enum CoherenceState {
22    #[default]
23    Healthy,
24    GettingCrowded,
25    RefreshingContext,
26    VerifyingRecentWork,
27    ResettingPlan,
28}
29
30impl CoherenceState {
31    #[must_use]
32    pub fn label(self) -> &'static str {
33        match self {
34            Self::Healthy => "healthy",
35            Self::GettingCrowded => "getting crowded",
36            Self::RefreshingContext => "refreshing context",
37            Self::VerifyingRecentWork => "verifying recent work",
38            Self::ResettingPlan => "resetting plan",
39        }
40    }
41
42    #[must_use]
43    pub fn description(self) -> &'static str {
44        match self {
45            Self::Healthy => "The session is stable and focused.",
46            Self::GettingCrowded => "The session is approaching context pressure.",
47            Self::RefreshingContext => "The engine is refreshing context before continuing.",
48            Self::VerifyingRecentWork => {
49                "The engine is checking recent tool results before continuing."
50            }
51            Self::ResettingPlan => {
52                "The engine is rebuilding from canonical context and replanning."
53            }
54        }
55    }
56}
57
58/// Synthetic input to the coherence reducer.
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
60pub enum CoherenceSignal {
61    CapacityDecision {
62        risk_band: RiskBand,
63        action: GuardrailAction,
64        cooldown_blocked: bool,
65    },
66    CapacityIntervention {
67        action: GuardrailAction,
68    },
69    CompactionStarted,
70    CompactionCompleted,
71    CompactionFailed,
72}
73
74/// Pure transition function for the plain-language coherence ladder.
75#[must_use]
76pub fn next_coherence_state(current: CoherenceState, signal: CoherenceSignal) -> CoherenceState {
77    match signal {
78        CoherenceSignal::CompactionStarted => CoherenceState::RefreshingContext,
79        CoherenceSignal::CompactionCompleted => CoherenceState::Healthy,
80        CoherenceSignal::CompactionFailed => CoherenceState::GettingCrowded,
81        CoherenceSignal::CapacityIntervention { action }
82        | CoherenceSignal::CapacityDecision { action, .. } => match action {
83            GuardrailAction::NoIntervention => match signal {
84                CoherenceSignal::CapacityDecision {
85                    risk_band,
86                    cooldown_blocked,
87                    ..
88                } => {
89                    if cooldown_blocked {
90                        return current;
91                    }
92                    match risk_band {
93                        RiskBand::Low => CoherenceState::Healthy,
94                        RiskBand::Medium | RiskBand::High => CoherenceState::GettingCrowded,
95                    }
96                }
97                _ => current,
98            },
99            GuardrailAction::TargetedContextRefresh => CoherenceState::RefreshingContext,
100            GuardrailAction::VerifyWithToolReplay => CoherenceState::VerifyingRecentWork,
101            GuardrailAction::VerifyAndReplan => CoherenceState::ResettingPlan,
102        },
103    }
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109
110    #[test]
111    fn synthetic_capacity_event_log_drives_plain_language_ladder() {
112        let log = [
113            CoherenceSignal::CapacityDecision {
114                risk_band: RiskBand::Low,
115                action: GuardrailAction::NoIntervention,
116                cooldown_blocked: false,
117            },
118            CoherenceSignal::CapacityDecision {
119                risk_band: RiskBand::Medium,
120                action: GuardrailAction::NoIntervention,
121                cooldown_blocked: false,
122            },
123            CoherenceSignal::CapacityDecision {
124                risk_band: RiskBand::Medium,
125                action: GuardrailAction::TargetedContextRefresh,
126                cooldown_blocked: false,
127            },
128            CoherenceSignal::CompactionCompleted,
129            CoherenceSignal::CapacityDecision {
130                risk_band: RiskBand::High,
131                action: GuardrailAction::VerifyWithToolReplay,
132                cooldown_blocked: false,
133            },
134            CoherenceSignal::CapacityDecision {
135                risk_band: RiskBand::High,
136                action: GuardrailAction::VerifyAndReplan,
137                cooldown_blocked: false,
138            },
139        ];
140
141        let mut state = CoherenceState::Healthy;
142        let mut states = Vec::new();
143        for signal in log {
144            state = next_coherence_state(state, signal);
145            states.push(state);
146        }
147
148        assert_eq!(
149            states,
150            vec![
151                CoherenceState::Healthy,
152                CoherenceState::GettingCrowded,
153                CoherenceState::RefreshingContext,
154                CoherenceState::Healthy,
155                CoherenceState::VerifyingRecentWork,
156                CoherenceState::ResettingPlan,
157            ]
158        );
159    }
160}