use crate::cognition::locus_coeruleus::LocusCoeruleus;
use crate::cognition::resource_allocator::{
allocate_context_budget, compute_resource_pressure, AllocatorContext, ModelTier,
};
use crate::cognition::adaptive_thresholds::build_threshold_context;
use crate::cognition::thalamic_gate::classify_gate_with_feedback;
use crate::cognition::world_model::perceive;
use crate::math::clamp;
use crate::types::gate::{GateContext, GateContextWithFeedback, RecentMessage};
use crate::types::world::WorldModel;
pub const CONVERGENCE_EPSILON: f64 = 0.01;
pub const MAX_ITERATIONS: usize = 5;
pub const DAMPING_ALPHA: f64 = 0.5;
#[derive(Debug, Clone)]
pub struct ConvergenceResult {
pub model: WorldModel,
pub iterations: usize,
pub converged: bool,
pub final_delta: f64,
}
#[derive(Debug, Clone, Default)]
pub struct ConvergenceContext {
pub model_tier: ModelTier,
pub fok_average: Option<f64>,
pub has_graph_data: bool,
pub message_count: usize,
}
pub fn converge(
model: &WorldModel,
message: &str,
recent_messages: &[RecentMessage],
lc: &mut LocusCoeruleus,
ctx: &ConvergenceContext,
) -> ConvergenceResult {
let mut current = model.clone();
let mut final_delta = 1.0;
let mut iterations = 0;
let mut previous_computed: Option<WorldModel> = None;
for i in 0..MAX_ITERATIONS {
iterations = i + 1;
let computed = run_one_iteration(
¤t,
message,
recent_messages,
lc,
ctx,
);
if let Some(ref prev) = previous_computed {
final_delta = compute_max_delta(prev, &computed);
if final_delta < CONVERGENCE_EPSILON {
current = damp_state(&computed, ¤t, DAMPING_ALPHA);
break;
}
}
previous_computed = Some(computed.clone());
current = damp_state(&computed, ¤t, DAMPING_ALPHA);
}
ConvergenceResult {
model: current,
iterations,
converged: final_delta < CONVERGENCE_EPSILON,
final_delta,
}
}
fn run_one_iteration(
model: &WorldModel,
message: &str,
recent_messages: &[RecentMessage],
lc: &mut LocusCoeruleus,
ctx: &ConvergenceContext,
) -> WorldModel {
let after_perceive = perceive(model, message);
let threshold_ctx = build_threshold_context(
after_perceive.sensory_pe,
after_perceive.belief.affect.arousal,
model.gate.confidence,
after_perceive.pe_volatility,
Some(after_perceive.belief.affect.valence),
Some(after_perceive.body_budget),
);
let base_ctx = GateContext {
message,
recent_messages,
arousal: threshold_ctx.arousal, };
let gate_ctx = GateContextWithFeedback {
base: base_ctx,
resource_pressure: after_perceive.resource_pressure,
previous_gate: Some(&model.gate),
};
let gate_result = classify_gate_with_feedback(&gate_ctx);
lc.set_arousal(after_perceive.belief.affect.arousal);
let rpe_adjusted = clamp(
gate_result.confidence + after_perceive.response_rpe * 0.15,
0.0,
1.0,
);
lc.nudge_gain_from_confidence(rpe_adjusted);
let alloc_ctx = AllocatorContext {
query: message,
gate_result: Some(&gate_result),
gain_mode: lc.gain_mode(),
arousal: after_perceive.belief.affect.arousal,
fok_average: ctx.fok_average,
model_tier: ctx.model_tier,
has_graph_data: ctx.has_graph_data,
active_file_count: 0, pinned_count: 0,
has_prospective: false,
message_count: ctx.message_count,
has_threat_topics: false,
};
let allocation = allocate_context_budget(&alloc_ctx, &[]);
let resource_pressure = compute_resource_pressure(allocation.as_ref());
let mut result = after_perceive;
result.gate = gate_result;
result.resource_pressure = resource_pressure;
result
}
fn compute_max_delta(before: &WorldModel, after: &WorldModel) -> f64 {
let deltas = [
(after.belief.affect.arousal - before.belief.affect.arousal).abs(),
(after.belief.affect.certainty - before.belief.affect.certainty).abs(),
(after.belief.affect.sustained - before.belief.affect.sustained).abs(),
(after.sensory_pe - before.sensory_pe).abs(),
(after.resource_pressure - before.resource_pressure).abs(),
(after.gate.confidence - before.gate.confidence).abs(),
if after.gate.gate != before.gate.gate {
1.0
} else {
0.0
},
];
deltas
.iter()
.cloned()
.fold(0.0_f64, f64::max)
}
fn damp_state(computed: &WorldModel, previous: &WorldModel, alpha: f64) -> WorldModel {
let blend = |new: f64, old: f64| alpha * new + (1.0 - alpha) * old;
let mut result = computed.clone();
result.belief.affect.arousal = blend(computed.belief.affect.arousal, previous.belief.affect.arousal);
result.belief.affect.certainty = blend(computed.belief.affect.certainty, previous.belief.affect.certainty);
result.belief.affect.sustained = blend(computed.belief.affect.sustained, previous.belief.affect.sustained);
result.sensory_pe = blend(computed.sensory_pe, previous.sensory_pe);
result.resource_pressure = blend(computed.resource_pressure, previous.resource_pressure);
result.gate.confidence = blend(computed.gate.confidence, previous.gate.confidence);
result
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::gate::GateType;
#[test]
fn convergence_simple_message() {
let model = WorldModel::new("test".into());
let mut lc = LocusCoeruleus::new();
let result = converge(
&model,
"Hello world",
&[],
&mut lc,
&ConvergenceContext::default(),
);
assert!(result.iterations >= 1);
assert!(result.iterations <= MAX_ITERATIONS);
assert!(result.converged || result.iterations <= 3);
}
#[test]
fn convergence_high_arousal() {
let model = WorldModel::new("test".into());
let mut lc = LocusCoeruleus::new();
let result = converge(
&model,
"ERROR! Everything is broken!!! HELP!!!",
&[],
&mut lc,
&ConvergenceContext::default(),
);
assert!(result.model.belief.affect.arousal > 0.0);
assert_eq!(result.model.gate.gate, GateType::Urgent);
}
#[test]
fn convergence_max_iterations() {
let model = WorldModel::new("test".into());
let mut lc = LocusCoeruleus::new();
let result = converge(
&model,
"Test message",
&[],
&mut lc,
&ConvergenceContext::default(),
);
assert!(result.iterations <= MAX_ITERATIONS);
}
#[test]
fn damping_blends_values() {
let mut a = WorldModel::new("test".into());
let mut b = WorldModel::new("test".into());
a.belief.affect.arousal = 1.0;
b.belief.affect.arousal = 0.0;
let damped = damp_state(&a, &b, 0.5);
assert!((damped.belief.affect.arousal - 0.5).abs() < f64::EPSILON);
}
#[test]
fn max_delta_identical() {
let model = WorldModel::new("test".into());
assert!(compute_max_delta(&model, &model) < f64::EPSILON);
}
#[test]
fn max_delta_gate_change() {
let mut a = WorldModel::new("test".into());
let mut b = WorldModel::new("test".into());
a.gate.gate = GateType::Novel;
b.gate.gate = GateType::Urgent;
assert_eq!(compute_max_delta(&a, &b), 1.0);
}
#[test]
fn convergence_result_model_has_gate() {
let model = WorldModel::new("test".into());
let mut lc = LocusCoeruleus::new();
let result = converge(
&model,
"Just a normal message about programming",
&[],
&mut lc,
&ConvergenceContext::default(),
);
assert!(!result.model.gate.reason.is_empty());
}
#[test]
fn lc_gain_mode_stable_on_repeated_converge() {
let model = WorldModel::new("test".into());
let mut lc = LocusCoeruleus::new();
let _ = converge(
&model,
"Neutral message about weather",
&[],
&mut lc,
&ConvergenceContext::default(),
);
let mode_first = lc.gain_mode();
let _ = converge(
&model,
"Neutral message about weather",
&[],
&mut lc,
&ConvergenceContext::default(),
);
let mode_second = lc.gain_mode();
assert_eq!(
mode_first, mode_second,
"LC gain_mode should be stable on repeated convergence; got \
{mode_first:?} then {mode_second:?}"
);
}
#[test]
fn convergence_reports_converged_flag_meaningfully() {
let model = WorldModel::new("test".into());
let mut lc = LocusCoeruleus::new();
let result = converge(
&model,
"Simple test message",
&[],
&mut lc,
&ConvergenceContext::default(),
);
if result.converged {
assert!(
result.final_delta < CONVERGENCE_EPSILON,
"converged=true claim violated: final_delta={} >= epsilon={}",
result.final_delta, CONVERGENCE_EPSILON
);
}
}
}