1use std::collections::BTreeMap;
14
15use serde::Serialize;
16
17use crate::residual::{CorrectionDirection, ResidualEvent};
18use crate::stability::StabilityParameters;
19
20pub type Evidence = BTreeMap<String, String>;
22
23pub type CorrectionDirectionSet = Vec<CorrectionDirection>;
25
26#[derive(Debug, Clone, PartialEq)]
28pub struct AgentBarrierResult {
29 pub ok: bool,
30 pub score: f64,
32 pub residuals: Vec<ResidualEvent>,
33 pub feedback: CorrectionDirectionSet,
34 pub evidence: Evidence,
35}
36
37impl AgentBarrierResult {
38 pub fn new(ok: bool, score: f64) -> Self {
39 Self {
40 ok,
41 score,
42 residuals: Vec::new(),
43 feedback: Vec::new(),
44 evidence: Evidence::new(),
45 }
46 }
47
48 pub fn with_residuals(mut self, residuals: Vec<ResidualEvent>) -> Self {
49 self.residuals = residuals;
50 self
51 }
52
53 pub fn with_feedback(mut self, feedback: CorrectionDirectionSet) -> Self {
54 self.feedback = feedback;
55 self
56 }
57
58 pub fn into_srbn(
62 self,
63 name: impl Into<String>,
64 ) -> srbn::SrbnResult<srbn::BarrierResult<CorrectionDirectionSet, Evidence>> {
65 let feedback = self
66 .feedback
67 .first()
68 .map(|d| d.instruction.clone())
69 .unwrap_or_default();
70 let correction = if self.feedback.is_empty() {
71 None
72 } else {
73 Some(self.feedback)
74 };
75 srbn::BarrierResult::new(
76 name,
77 self.ok,
78 self.score,
79 feedback,
80 correction,
81 self.evidence,
82 )
83 }
84}
85
86#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
89#[serde(rename_all = "snake_case")]
90pub enum AgentStabilizationStatus {
91 Stable,
92 Descended,
93 Stopped,
94 Exhausted,
95 Degraded,
96 ReplanRequired,
97}
98
99impl From<srbn::Status> for AgentStabilizationStatus {
100 fn from(status: srbn::Status) -> Self {
101 match status {
102 srbn::Status::Stable => AgentStabilizationStatus::Stable,
103 srbn::Status::Stopped => AgentStabilizationStatus::Stopped,
104 srbn::Status::Exhausted => AgentStabilizationStatus::Exhausted,
105 }
106 }
107}
108
109pub fn policy(params: &StabilityParameters, max_attempts: usize) -> srbn::Policy {
116 srbn::Policy {
117 max_attempts: max_attempts.max(1),
118 score_tolerance: params.energy_tolerance,
119 require_descent: true,
120 on_no_descent: srbn::OnNoDescent::Stop,
121 min_descent: params.rho_gate,
122 }
123}
124
125pub fn policy_restore_best(params: &StabilityParameters, max_attempts: usize) -> srbn::Policy {
136 srbn::Policy {
137 on_no_descent: srbn::OnNoDescent::Reject,
138 ..policy(params, max_attempts)
139 }
140}
141
142pub fn stabilize<State, B, U>(
148 initial: State,
149 mut barrier: B,
150 updater: U,
151 params: &StabilityParameters,
152 max_attempts: usize,
153) -> crate::error::Result<srbn::StabilizationResult<State, CorrectionDirectionSet, Evidence>>
154where
155 State: Clone,
156 B: FnMut(&State) -> AgentBarrierResult,
157 U: FnMut(
158 State,
159 &srbn::BarrierResult<CorrectionDirectionSet, Evidence>,
160 ) -> srbn::SrbnResult<State>,
161{
162 let srbn_barrier = move |state: &State| barrier(state).into_srbn("agent-barrier");
163 let result = srbn::stabilize(initial, srbn_barrier, updater, policy(params, max_attempts))?;
164 Ok(result)
165}
166
167pub fn stabilize_realized<State, B, U, R>(
177 initial: State,
178 mut barrier: B,
179 updater: U,
180 realizer: R,
181 params: &StabilityParameters,
182 max_attempts: usize,
183) -> crate::error::Result<srbn::StabilizationResult<State, CorrectionDirectionSet, Evidence>>
184where
185 State: Clone,
186 B: FnMut(&State) -> AgentBarrierResult,
187 U: FnMut(
188 State,
189 &srbn::BarrierResult<CorrectionDirectionSet, Evidence>,
190 ) -> srbn::SrbnResult<State>,
191 R: srbn::Realizer<State>,
192{
193 let srbn_barrier = move |state: &State| barrier(state).into_srbn("agent-barrier");
194 let result = srbn::stabilize_realized(
195 initial,
196 srbn_barrier,
197 updater,
198 realizer,
199 policy_restore_best(params, max_attempts),
200 )?;
201 Ok(result)
202}
203
204pub fn trace_to_json<State>(
207 result: &srbn::StabilizationResult<State, CorrectionDirectionSet, Evidence>,
208) -> crate::error::Result<String>
209where
210 State: Serialize,
211{
212 srbn_serde::stabilization_result_json(result)
213 .map_err(|e| crate::error::SdkError::Kernel(format!("trace serialization failed: {e}")))
214}
215
216#[cfg(test)]
217mod tests {
218 use super::*;
219
220 #[test]
221 fn maps_kernel_status() {
222 assert_eq!(
223 AgentStabilizationStatus::from(srbn::Status::Stable),
224 AgentStabilizationStatus::Stable
225 );
226 assert_eq!(
227 AgentStabilizationStatus::from(srbn::Status::Stopped),
228 AgentStabilizationStatus::Stopped
229 );
230 assert_eq!(
231 AgentStabilizationStatus::from(srbn::Status::Exhausted),
232 AgentStabilizationStatus::Exhausted
233 );
234 }
235
236 #[test]
237 fn stabilizes_descending_energy_through_kernel() {
238 let params = StabilityParameters::measured(0.5, 0.0);
241 let barrier = |state: &i64| {
242 let v = (*state as f64) * (*state as f64);
243 AgentBarrierResult::new(*state == 0, v)
244 };
245 let updater = |state: i64, _b: &srbn::BarrierResult<CorrectionDirectionSet, Evidence>| {
246 Ok(state - state.signum())
247 };
248 let result = stabilize(3, barrier, updater, ¶ms, 10).unwrap();
249 assert_eq!(result.status, srbn::Status::Stable);
250 assert_eq!(result.state, 0);
251
252 let json = trace_to_json(&result).unwrap();
254 assert!(json.contains("\"status\":\"stable\""));
255 assert!(json.contains("\"attempts\""));
256 }
257
258 #[test]
259 fn realized_loop_gates_on_realized_state_and_records_residual() {
260 let params = StabilityParameters::measured(0.05, 0.0);
264 let barrier = |state: &f64| AgentBarrierResult::new(state.abs() < 0.05, state * state);
265 let updater = |state: f64, _b: &srbn::BarrierResult<CorrectionDirectionSet, Evidence>| {
266 Ok(state - 0.4 * state.signum())
267 };
268 let realizer = |proposed: f64| {
269 let realized = (proposed * 4.0).round() / 4.0;
270 Ok(srbn::Realization::measured(
271 realized,
272 (proposed - realized).abs(),
273 ))
274 };
275 let result = stabilize_realized(1.1, barrier, updater, realizer, ¶ms, 12).unwrap();
276 for attempt in result.trace() {
278 let scaled = attempt.state * 4.0;
279 assert!(
280 (scaled - scaled.round()).abs() < 1e-9,
281 "off-lattice: {}",
282 attempt.state
283 );
284 }
285 let max_r = result
286 .max_realizability_residual()
287 .expect("measured residuals");
288 assert!(
289 max_r <= 0.125 + 1e-9,
290 "lattice projection bound exceeded: {max_r}"
291 );
292 }
293
294 #[test]
295 fn restore_best_policy_measures_descent_against_best() {
296 let params = StabilityParameters::measured(0.5, 0.0);
297 let p = policy_restore_best(¶ms, 7);
298 assert_eq!(p.on_no_descent, srbn::OnNoDescent::Reject);
299 assert!(p.require_descent);
300 assert_eq!(p.max_attempts, 7);
301 }
302
303 #[test]
304 fn hash_chain_commits_and_verifies() {
305 let mut ledger = srbn_ledger::MerkleLedger::new();
308 ledger.commit("first trace payload");
309 ledger.commit("second trace payload");
310 assert!(ledger.verify());
311 assert_ne!(ledger.head(), srbn_ledger::GENESIS_HASH);
312 }
313
314 #[test]
315 fn rejects_non_finite_score_at_barrier() {
316 let params = StabilityParameters::measured(0.5, 0.0);
317 let barrier = |_state: &i64| AgentBarrierResult::new(false, f64::NAN);
318 let updater =
319 |state: i64, _b: &srbn::BarrierResult<CorrectionDirectionSet, Evidence>| Ok(state);
320 assert!(stabilize(1, barrier, updater, ¶ms, 3).is_err());
322 }
323}