1use schemars::JsonSchema;
14use serde::{Deserialize, Serialize};
15
16use crate::capacity::{GuardrailAction, RiskBand};
17
18#[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#[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#[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}