use std::collections::HashMap;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum SignalSource {
Anomaly,
Accumulator,
Feedback,
Confidence,
Alignment,
OperationalLoad,
ExecutionStress,
CognitiveFlow,
ResourcePressure,
VoiceEmotion,
}
impl std::fmt::Display for SignalSource {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Anomaly => write!(f, "anomaly"),
Self::Accumulator => write!(f, "accumulator"),
Self::Feedback => write!(f, "feedback"),
Self::Confidence => write!(f, "confidence"),
Self::Alignment => write!(f, "alignment"),
Self::OperationalLoad => write!(f, "operational_load"),
Self::ExecutionStress => write!(f, "execution_stress"),
Self::CognitiveFlow => write!(f, "cognitive_flow"),
Self::ResourcePressure => write!(f, "resource_pressure"),
Self::VoiceEmotion => write!(f, "voice_emotion"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InteroceptiveSignal {
pub source: SignalSource,
pub domain: Option<String>,
pub valence: f64,
pub arousal: f64,
pub timestamp: DateTime<Utc>,
pub context: Option<SignalContext>,
}
impl InteroceptiveSignal {
pub fn new(
source: SignalSource,
domain: Option<String>,
valence: f64,
arousal: f64,
) -> Self {
Self {
source,
domain,
valence: valence.clamp(-1.0, 1.0),
arousal: arousal.clamp(0.0, 1.0),
timestamp: Utc::now(),
context: None,
}
}
pub fn with_context(mut self, ctx: SignalContext) -> Self {
self.context = Some(ctx);
self
}
pub fn is_negative(&self) -> bool {
self.valence < -0.3
}
pub fn is_urgent(&self) -> bool {
self.arousal > 0.7
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum SignalContext {
AnomalyDetected {
metric: String,
z_score: f64,
baseline_mean: f64,
},
EmotionalEvent {
event_description: String,
},
ActionOutcome {
action: String,
success: bool,
cumulative_score: f64,
},
RecallConfidence {
query: String,
score: f64,
},
DriveAlignment {
content_snippet: String,
alignment_score: f64,
},
TokenPressure {
budget_used_pct: f64,
tokens_per_second: f64,
budget_runway_secs: f64,
},
LoopStress {
loop_depth: u32,
retry_count: u32,
tool_failure_rate: f64,
consecutive_failures: u32,
},
TaskFlow {
task_completion_rate: f64,
response_latency_ms: u64,
session_duration_secs: u64,
},
SystemPressure {
disk_free_gb: f64,
queue_depth: u32,
},
VoiceEmotion {
primary_emotion: String,
confidence: f64,
all_scores: HashMap<String, f64>,
speaker_id: Option<String>,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DomainState {
pub domain: String,
pub valence_trend: f64,
pub anomaly_level: f64,
pub action_success_rate: f64,
pub alignment_score: f64,
pub confidence: f64,
pub signal_count: u64,
pub last_updated: DateTime<Utc>,
}
impl DomainState {
pub fn new(domain: impl Into<String>) -> Self {
Self {
domain: domain.into(),
valence_trend: 0.0,
anomaly_level: 0.0,
action_success_rate: 0.5,
alignment_score: 0.5,
confidence: 0.5,
signal_count: 0,
last_updated: Utc::now(),
}
}
pub fn update(&mut self, signal: &InteroceptiveSignal, alpha: f64) {
let alpha = alpha.clamp(0.0, 1.0);
self.valence_trend = alpha * signal.valence + (1.0 - alpha) * self.valence_trend;
match signal.source {
SignalSource::Anomaly => {
self.anomaly_level = alpha * signal.arousal * 3.0
+ (1.0 - alpha) * self.anomaly_level;
}
SignalSource::Feedback => {
let rate = (signal.valence + 1.0) / 2.0;
self.action_success_rate =
alpha * rate + (1.0 - alpha) * self.action_success_rate;
}
SignalSource::Alignment => {
let score = (signal.valence + 1.0) / 2.0;
self.alignment_score =
alpha * score + (1.0 - alpha) * self.alignment_score;
}
SignalSource::Confidence => {
let conf = (signal.valence + 1.0) / 2.0;
self.confidence = alpha * conf + (1.0 - alpha) * self.confidence;
}
SignalSource::Accumulator => {
}
SignalSource::OperationalLoad
| SignalSource::ExecutionStress
| SignalSource::CognitiveFlow
| SignalSource::ResourcePressure
| SignalSource::VoiceEmotion => {
if signal.arousal > 0.5 {
self.anomaly_level =
alpha * signal.arousal * 2.0 + (1.0 - alpha) * self.anomaly_level;
}
}
}
self.signal_count += 1;
self.last_updated = Utc::now();
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SomaticMarker {
pub situation_hash: u64,
pub valence: f64,
pub encounter_count: u32,
pub last_accessed: DateTime<Utc>,
}
impl SomaticMarker {
pub fn new(situation_hash: u64, valence: f64) -> Self {
Self {
situation_hash,
valence,
encounter_count: 1,
last_accessed: Utc::now(),
}
}
pub fn update(&mut self, new_valence: f64) {
self.encounter_count += 1;
let n = self.encounter_count as f64;
self.valence += (new_valence - self.valence) / n;
self.last_accessed = Utc::now();
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InteroceptiveState {
pub domain_states: HashMap<String, DomainState>,
pub global_arousal: f64,
pub buffer_size: usize,
pub active_markers: Vec<SomaticMarker>,
pub timestamp: DateTime<Utc>,
}
impl InteroceptiveState {
pub fn to_prompt_section(&self) -> String {
if self.domain_states.is_empty() {
return String::from("Internal state: no data yet.");
}
let mut lines = vec!["## Internal State (Interoceptive)".to_string()];
let mut domains: Vec<_> = self.domain_states.values().collect();
domains.sort_by(|a, b| a.domain.cmp(&b.domain));
for ds in &domains {
let sentiment = if ds.valence_trend > 0.3 {
"positive"
} else if ds.valence_trend < -0.3 {
"negative"
} else {
"neutral"
};
lines.push(format!(
"- **{}**: {} (valence {:.2}, anomaly {:.1}, confidence {:.0}%, alignment {:.0}%)",
ds.domain,
sentiment,
ds.valence_trend,
ds.anomaly_level,
ds.confidence * 100.0,
ds.alignment_score * 100.0,
));
}
if self.global_arousal > 0.5 {
lines.push(format!(
"- ⚡ Elevated arousal: {:.0}%",
self.global_arousal * 100.0
));
}
if !self.active_markers.is_empty() {
lines.push(format!(
"- Somatic markers active: {} situation(s) recognized",
self.active_markers.len()
));
}
lines.join("\n")
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum RegulationAction {
SoulUpdateSuggestion {
domain: String,
reason: String,
valence_trend: f64,
},
RetrievalAdjustment {
expand_search: bool,
reason: String,
},
BehaviorShift {
action: String,
recommendation: String,
success_rate: f64,
},
Alert {
severity: AlertSeverity,
message: String,
domains: Vec<String>,
},
IdentityEvolutionSuggestion {
aspect: IdentityAspect,
observation: String,
domains: Vec<String>,
confidence: f64,
suggestion: String,
},
HeartbeatFrequencyAdjustment {
direction: HeartbeatAdjustDirection,
interval_multiplier: f64,
reason: String,
domains: Vec<String>,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum HeartbeatAdjustDirection {
Increase,
Decrease,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum IdentityAspect {
Capability,
BehavioralPattern,
PersonalityTrait,
DomainSpecialization,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum AlertSeverity {
Low,
Medium,
High,
}
impl std::fmt::Display for AlertSeverity {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Low => write!(f, "low"),
Self::Medium => write!(f, "medium"),
Self::High => write!(f, "high"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AdaptiveBaseline {
pub count: u64,
pub mean: f64,
m2: f64,
pub min_samples: u64,
pub decay: f64,
}
impl AdaptiveBaseline {
pub fn new(min_samples: u64) -> Self {
Self {
count: 0,
mean: 0.0,
m2: 0.0,
min_samples,
decay: 0.0,
}
}
pub fn with_decay(min_samples: u64, decay: f64) -> Self {
Self {
count: 0,
mean: 0.0,
m2: 0.0,
min_samples,
decay: decay.clamp(0.0, 0.5),
}
}
pub fn observe(&mut self, value: f64) {
self.count += 1;
if self.decay > 0.0 && self.count > 1 {
let alpha = self.decay;
let delta = value - self.mean;
self.mean += alpha * delta;
let delta2 = value - self.mean;
self.m2 = (1.0 - alpha) * (self.m2 + alpha * delta * delta2);
} else {
let delta = value - self.mean;
self.mean += delta / self.count as f64;
let delta2 = value - self.mean;
self.m2 += delta * delta2;
}
}
pub fn is_calibrated(&self) -> bool {
self.count >= self.min_samples
}
pub fn variance(&self) -> f64 {
if self.count < 2 {
return 0.0;
}
if self.decay > 0.0 {
self.m2.max(0.0)
} else {
(self.m2 / self.count as f64).max(0.0)
}
}
pub fn stddev(&self) -> f64 {
self.variance().sqrt()
}
pub fn sigma_deviation(&self, value: f64) -> Option<f64> {
if !self.is_calibrated() {
return None;
}
let sd = self.stddev();
if sd < 1e-10 {
if (value - self.mean).abs() < 1e-10 {
return Some(0.0);
}
return Some(5.0);
}
Some((value - self.mean).abs() / sd)
}
pub fn deviation_level(&self, value: f64) -> DeviationLevel {
match self.sigma_deviation(value) {
None => DeviationLevel::Uncalibrated,
Some(sigma) if sigma < 1.5 => DeviationLevel::Normal,
Some(sigma) if sigma < 2.5 => DeviationLevel::Elevated,
Some(sigma) if sigma < 3.5 => DeviationLevel::High,
Some(_) => DeviationLevel::Extreme,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum DeviationLevel {
Uncalibrated,
Normal,
Elevated,
High,
Extreme,
}
impl DeviationLevel {
pub fn is_actionable(&self) -> bool {
matches!(self, Self::High | Self::Extreme)
}
pub fn is_elevated(&self) -> bool {
matches!(self, Self::Elevated | Self::High | Self::Extreme)
}
}
impl std::fmt::Display for DeviationLevel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Uncalibrated => write!(f, "uncalibrated"),
Self::Normal => write!(f, "normal"),
Self::Elevated => write!(f, "elevated"),
Self::High => write!(f, "high"),
Self::Extreme => write!(f, "extreme"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn signal_clamps_values() {
let sig = InteroceptiveSignal::new(SignalSource::Anomaly, None, 5.0, -2.0);
assert_eq!(sig.valence, 1.0);
assert_eq!(sig.arousal, 0.0);
let sig = InteroceptiveSignal::new(SignalSource::Anomaly, None, -5.0, 3.0);
assert_eq!(sig.valence, -1.0);
assert_eq!(sig.arousal, 1.0);
}
#[test]
fn signal_negative_and_urgent() {
let calm_positive = InteroceptiveSignal::new(
SignalSource::Accumulator,
Some("coding".into()),
0.5,
0.2,
);
assert!(!calm_positive.is_negative());
assert!(!calm_positive.is_urgent());
let alarming = InteroceptiveSignal::new(
SignalSource::Anomaly,
Some("trading".into()),
-0.8,
0.9,
);
assert!(alarming.is_negative());
assert!(alarming.is_urgent());
}
#[test]
fn signal_with_context() {
let sig = InteroceptiveSignal::new(SignalSource::Anomaly, None, -0.5, 0.8)
.with_context(SignalContext::AnomalyDetected {
metric: "recall_latency".into(),
z_score: 2.5,
baseline_mean: 120.0,
});
assert!(sig.context.is_some());
}
#[test]
fn domain_state_update_ewma() {
let mut ds = DomainState::new("coding");
assert_eq!(ds.valence_trend, 0.0);
let alpha = 0.3;
for _ in 0..10 {
let sig = InteroceptiveSignal::new(
SignalSource::Accumulator,
Some("coding".into()),
0.8,
0.2,
);
ds.update(&sig, alpha);
}
assert!(ds.valence_trend > 0.6, "got {}", ds.valence_trend);
assert_eq!(ds.signal_count, 10);
}
#[test]
fn domain_state_source_specific_updates() {
let mut ds = DomainState::new("trading");
let alpha = 0.5;
let feedback = InteroceptiveSignal::new(
SignalSource::Feedback,
Some("trading".into()),
0.6, 0.3,
);
ds.update(&feedback, alpha);
assert!((ds.action_success_rate - 0.65).abs() < 0.01);
let anomaly = InteroceptiveSignal::new(
SignalSource::Anomaly,
Some("trading".into()),
-0.5,
0.9,
);
ds.update(&anomaly, alpha);
assert!((ds.anomaly_level - 1.35).abs() < 0.01);
}
#[test]
fn somatic_marker_incremental_mean() {
let mut marker = SomaticMarker::new(12345, 0.5);
assert_eq!(marker.encounter_count, 1);
assert_eq!(marker.valence, 0.5);
marker.update(-0.5);
assert_eq!(marker.encounter_count, 2);
assert!((marker.valence - 0.0).abs() < f64::EPSILON);
marker.update(0.5);
assert_eq!(marker.encounter_count, 3);
assert!((marker.valence - 1.0 / 6.0).abs() < 0.01);
}
#[test]
fn interoceptive_state_prompt_output() {
let mut state = InteroceptiveState {
domain_states: HashMap::new(),
global_arousal: 0.3,
buffer_size: 42,
active_markers: vec![],
timestamp: Utc::now(),
};
assert_eq!(state.to_prompt_section(), "Internal state: no data yet.");
let mut ds = DomainState::new("coding");
ds.valence_trend = 0.6;
ds.confidence = 0.85;
ds.alignment_score = 0.9;
state.domain_states.insert("coding".into(), ds);
let prompt = state.to_prompt_section();
assert!(prompt.contains("coding"));
assert!(prompt.contains("positive"));
assert!(!prompt.contains("arousal")); }
#[test]
fn baseline_uncalibrated_before_min_samples() {
let mut bl = AdaptiveBaseline::new(5);
bl.observe(1.0);
bl.observe(2.0);
bl.observe(3.0);
assert!(!bl.is_calibrated());
assert_eq!(bl.sigma_deviation(5.0), None);
assert_eq!(bl.deviation_level(5.0), DeviationLevel::Uncalibrated);
}
#[test]
fn baseline_calibrates_at_min_samples() {
let mut bl = AdaptiveBaseline::new(5);
for v in [1.0, 2.0, 3.0, 4.0, 5.0] {
bl.observe(v);
}
assert!(bl.is_calibrated());
assert_eq!(bl.count, 5);
assert!((bl.mean - 3.0).abs() < 0.01);
assert!((bl.stddev() - std::f64::consts::SQRT_2).abs() < 0.01);
}
#[test]
fn baseline_sigma_deviation_correct() {
let mut bl = AdaptiveBaseline::new(5);
for v in [10.0, 10.0, 10.0, 10.0, 10.0, 12.0, 8.0, 10.0, 10.0, 10.0] {
bl.observe(v);
}
let _sd = bl.stddev();
let dev = bl.sigma_deviation(10.0).unwrap();
assert!(dev < 0.5, "at-mean deviation should be near 0, got {}", dev);
let dev_far = bl.sigma_deviation(15.0).unwrap();
assert!(dev_far > 2.0, "5 units from mean should be >2σ, got {}", dev_far);
}
#[test]
fn baseline_deviation_levels() {
let mut bl = AdaptiveBaseline::new(3);
for v in [-1.0, 0.0, 1.0, -1.0, 0.0, 1.0, -1.0, 0.0, 1.0, 0.0] {
bl.observe(v);
}
let sd = bl.stddev();
assert_eq!(bl.deviation_level(bl.mean), DeviationLevel::Normal);
assert_eq!(bl.deviation_level(bl.mean + 2.0 * sd), DeviationLevel::Elevated);
assert_eq!(bl.deviation_level(bl.mean + 3.0 * sd), DeviationLevel::High);
assert_eq!(bl.deviation_level(bl.mean + 4.0 * sd), DeviationLevel::Extreme);
}
#[test]
fn baseline_zero_variance_handling() {
let mut bl = AdaptiveBaseline::new(3);
for _ in 0..5 {
bl.observe(42.0);
}
assert!(bl.stddev() < 1e-10);
assert_eq!(bl.sigma_deviation(42.0), Some(0.0));
assert_eq!(bl.sigma_deviation(43.0), Some(5.0));
}
#[test]
fn baseline_with_decay_adapts() {
let mut bl = AdaptiveBaseline::with_decay(3, 0.1);
for _ in 0..10 {
bl.observe(10.0);
}
let mean_before = bl.mean;
for _ in 0..20 {
bl.observe(20.0);
}
assert!(bl.mean > mean_before + 3.0,
"mean should drift toward 20 with decay, got {}", bl.mean);
}
#[test]
fn deviation_level_actionable() {
assert!(!DeviationLevel::Uncalibrated.is_actionable());
assert!(!DeviationLevel::Normal.is_actionable());
assert!(!DeviationLevel::Elevated.is_actionable());
assert!(DeviationLevel::High.is_actionable());
assert!(DeviationLevel::Extreme.is_actionable());
}
}