Skip to main content

perspt_sdk/
kernel.rs

1//! SRBN kernel adapter (PSP-8 System 2 / Gate A).
2//!
3//! `perspt-sdk` uses the published [`srbn`] crate as the canonical SRBN kernel
4//! and [`srbn_serde`] for serializing traces that enter the Perspt ledger,
5//! rather than forking the kernel logic. This module is the narrow adapter that
6//! keeps Perspt's WorkGraph, residual, and capability types outside the SRBN
7//! crate while reusing its `stabilize` loop, `BarrierResult` contract, attempt
8//! traces, and terminal statuses.
9//!
10//! The authoritative acceptance gate is the *measured* gate in [`crate::gate`];
11//! `srbn` supplies the loop scaffolding and the serializable attempt trace.
12
13use std::collections::BTreeMap;
14
15use serde::Serialize;
16
17use crate::residual::{CorrectionDirection, ResidualEvent};
18use crate::stability::StabilityParameters;
19
20/// Deterministic string evidence map, matching [`srbn::Evidence`].
21pub type Evidence = BTreeMap<String, String>;
22
23/// A set of correction directions, the SDK's `Correction` payload.
24pub type CorrectionDirectionSet = Vec<CorrectionDirection>;
25
26/// The SDK-side barrier result (PSP-8 adapter type).
27#[derive(Debug, Clone, PartialEq)]
28pub struct AgentBarrierResult {
29    pub ok: bool,
30    /// The candidate's total energy `V` (used as the SRBN score).
31    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    /// Convert into the kernel's [`srbn::BarrierResult`]. The first
59    /// human-readable instruction becomes the feedback string; the full
60    /// correction set rides along as the typed `correction` payload.
61    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/// Terminal status of an SDK stabilization (PSP-8 adapter enum). Extends the
87/// kernel's [`srbn::Status`] with SDK-specific outcomes.
88#[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
109/// Build an [`srbn::Policy`] from SDK stability parameters.
110///
111/// The single descent tolerance `rho_gate` becomes the kernel's `min_descent`,
112/// and the energy tolerance becomes the kernel's `score_tolerance`. Descent is
113/// required and a stall stops the loop, producing a `Stopped` status that the
114/// SDK maps to a residual-certificate path.
115pub 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
125/// Build an [`srbn::Policy`] with the kernel's restore-best rule (PSP-9).
126///
127/// `OnNoDescent::Reject` keeps a non-descending attempt in the trace but
128/// issues the next correction from the best accepted attempt, measuring
129/// descent against the best accepted score. This is the executable
130/// conformance oracle for the SRBN gate's restore-best semantics; the live
131/// tool loop preserves the same semantics through
132/// [`crate::gate::AcceptedTrajectory`], which additionally tracks Paper II's
133/// separate rejection budget that the kernel's total-attempt policy cannot
134/// represent.
135pub 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
142/// Drive the kernel's [`srbn::stabilize`] loop over an arbitrary state, with the
143/// barrier producing an [`AgentBarrierResult`] (mapped to the kernel result).
144///
145/// Returns the kernel's [`srbn::StabilizationResult`] so callers can serialize
146/// the attempt trace through [`srbn_serde`] into the ledger.
147pub 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
167/// Drive the kernel's realized stabilization loop (Paper I Definition 12.2).
168///
169/// Every proposed state — including `initial` — passes through `realizer`
170/// before the barrier evaluates it, so the gate is always grounded in the
171/// state that would actually be committed. The realizer is **synchronous** by
172/// kernel contract: an async runtime must materialize the candidate first and
173/// hand this adapter an already-realized value (PSP-9 system 10). Residuals
174/// measured via [`srbn::Realization::measured`] surface through
175/// [`srbn::StabilizationResult::max_realizability_residual`].
176pub 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
204/// Serialize a kernel stabilization trace to JSON via [`srbn_serde`] for the
205/// ledger. Requires the state and evidence to be serializable.
206pub 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        // State is an integer "distance"; energy V = distance^2. Each step moves
239        // one toward zero. The kernel should reach a stable (zero-energy) state.
240        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, &params, 10).unwrap();
249        assert_eq!(result.status, srbn::Status::Stable);
250        assert_eq!(result.state, 0);
251
252        // Trace serializes through srbn-serde for the ledger.
253        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        // Proposals land on a 0.25 lattice: the realizer rounds and measures
261        // the projection distance. The barrier must only ever see lattice
262        // points, and the max measured residual estimates delta_r.
263        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, &params, 12).unwrap();
276        // Every attempt state is a lattice point.
277        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(&params, 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        // srbn-ledger supplies the chain half of the Merkle-ized state; the
306        // SDK ledger keeps per-artifact content addressing.
307        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        // The kernel validates scores and surfaces an error.
321        assert!(stabilize(1, barrier, updater, &params, 3).is_err());
322    }
323}