Skip to main content

qae_kernel/
domain.rs

1// SPDX-License-Identifier: BUSL-1.1
2//! Domain adapter trait — plugs domain-specific logic into the safety kernel.
3
4use crate::KernelResult;
5use std::collections::BTreeMap;
6
7use super::action::ProposedAction;
8use super::constraint::ConstraintChannel;
9
10/// Adapter that connects domain-specific knowledge to the safety kernel.
11///
12/// A domain adapter translates domain concepts (e.g., financial positions,
13/// robotic actuator states, LLM tool calls) into the kernel's state-vector
14/// representation and provides domain-specific constraint channels.
15pub trait DomainAdapter: Send + Sync {
16    /// Name of the domain (e.g., "finance", "agentic", "robotics").
17    fn domain_name(&self) -> &str;
18
19    /// Provide the constraint channels for this domain.
20    fn constraint_channels(&self) -> Vec<Box<dyn ConstraintChannel>>;
21
22    /// Map a proposed action's state deltas into a state vector.
23    fn map_action_to_state(&self, action: &dyn ProposedAction) -> KernelResult<Vec<f64>>;
24
25    /// Return the current (pre-action) state vector.
26    ///
27    /// Used by the certifier to provide accurate `current` state for regime
28    /// change detection. Default returns Err, which causes the certifier to
29    /// fall back to a zero vector of the correct length (safe fallback —
30    /// regime detection that depends on the delta will see a larger-than-real shift).
31    fn current_state(&self) -> KernelResult<Vec<f64>> {
32        Err(crate::KernelError::AdapterError(
33            "current_state not implemented".into(),
34        ))
35    }
36
37    /// Detect whether the proposed state change represents a regime change.
38    fn detect_regime_change(&self, current: &[f64], proposed: &[f64]) -> bool;
39
40    /// Format domain-specific payload data from channel margins.
41    fn format_domain_payload(
42        &self,
43        margins: &BTreeMap<String, f64>,
44    ) -> Option<serde_json::Value>;
45}
46
47#[cfg(test)]
48mod tests {
49    use super::*;
50    use crate::action::{ActionPriority, SimpleAction, StateDelta};
51    use chrono::Utc;
52
53    struct NullAdapter;
54
55    struct NullChannel;
56
57    impl ConstraintChannel for NullChannel {
58        fn name(&self) -> &str {
59            "null"
60        }
61        fn evaluate(&self, _state: &[f64]) -> KernelResult<f64> {
62            Ok(1.0)
63        }
64        fn dimension_names(&self) -> Vec<String> {
65            vec![]
66        }
67    }
68
69    impl DomainAdapter for NullAdapter {
70        fn domain_name(&self) -> &str {
71            "null"
72        }
73        fn constraint_channels(&self) -> Vec<Box<dyn ConstraintChannel>> {
74            vec![Box::new(NullChannel)]
75        }
76        fn map_action_to_state(&self, _action: &dyn ProposedAction) -> KernelResult<Vec<f64>> {
77            Ok(vec![])
78        }
79        fn detect_regime_change(&self, _current: &[f64], _proposed: &[f64]) -> bool {
80            false
81        }
82        fn format_domain_payload(
83            &self,
84            _margins: &BTreeMap<String, f64>,
85        ) -> Option<serde_json::Value> {
86            None
87        }
88    }
89
90    #[test]
91    fn null_adapter_provides_channels() {
92        let adapter = NullAdapter;
93        assert_eq!(adapter.domain_name(), "null");
94        assert_eq!(adapter.constraint_channels().len(), 1);
95    }
96
97    #[test]
98    fn null_adapter_maps_action() {
99        let adapter = NullAdapter;
100        let action = SimpleAction {
101            action_id: "a".into(),
102            agent_id: "b".into(),
103            proposed_at: Utc::now(),
104            state_deltas: vec![StateDelta {
105                dimension: "x".into(),
106                from_value: 0.0,
107                to_value: 1.0,
108            }],
109            priority: ActionPriority::Standard,
110        };
111        let state = adapter.map_action_to_state(&action).unwrap();
112        assert!(state.is_empty());
113    }
114
115    #[test]
116    fn adapter_is_object_safe() {
117        let adapter: Box<dyn DomainAdapter> = Box::new(NullAdapter);
118        assert_eq!(adapter.domain_name(), "null");
119    }
120}