cellstate_core/
intent.rs

1//! Intent Engineering - Machine-readable organizational purpose
2//!
3//! Defines the core types for encoding organizational intent that agents
4//! must align with. Used by the IntentAlignmentGate in the mutation pipeline.
5
6use serde::{Deserialize, Serialize};
7
8/// Autonomy level for agent decisions (from DeepMind research).
9///
10/// Determines how much independent authority an agent has.
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
12#[serde(rename_all = "snake_case")]
13#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
14pub enum AutonomyLevel {
15    /// Executes directed tasks with minimal discretion
16    #[default]
17    Operator,
18    /// Shared workflows with humans, can propose actions
19    Collaborator,
20    /// Analysis and recommendation only, no direct actions
21    Consultant,
22    /// Authorized to act within defined boundaries
23    Approver,
24    /// Monitors and alerts only, no actions permitted
25    Observer,
26}
27
28/// Target threshold for an alignment signal.
29#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
30#[serde(rename_all = "snake_case")]
31#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
32pub enum SignalTarget {
33    /// Value must be above threshold
34    Above(f64),
35    /// Value must be below threshold
36    Below(f64),
37    /// Value must be between min and max
38    Between { min: f64, max: f64 },
39}
40
41/// A data point that indicates successful intent alignment.
42#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
43#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
44pub struct AlignmentSignal {
45    /// Human-readable name (e.g., "retention_rate")
46    pub name: String,
47    /// Entity type to monitor (e.g., "customer_events")
48    pub source: String,
49    /// Field to measure (e.g., "30d_retention")
50    pub metric: String,
51    /// Threshold target
52    pub target: SignalTarget,
53}
54
55/// A resolution rule for when priorities conflict.
56///
57/// When an agent faces competing goals, resolution rules determine
58/// which goal takes precedence and when to escalate to humans.
59#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
60#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
61pub struct ResolutionRule {
62    /// Condition that triggers this rule (e.g., "customer_sentiment < 0.3")
63    pub condition: String,
64    /// Ordered list of goal names to prioritize when condition matches
65    pub priority: Vec<String>,
66    /// Human role to escalate to (if any)
67    #[serde(default, skip_serializing_if = "Option::is_none")]
68    pub escalate_to: Option<String>,
69    /// Maximum dollar value the agent can authorize under this rule
70    #[serde(default)]
71    pub max_authority: f64,
72}
73
74/// Delegation boundary -- what the agent can/cannot decide.
75#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
76#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
77pub struct DelegationBoundary {
78    /// Actions the agent is authorized to take independently
79    #[serde(default)]
80    pub authorized_actions: Vec<String>,
81    /// Actions that require human approval before execution
82    #[serde(default)]
83    pub requires_approval: Vec<String>,
84    /// Actions the agent must never take
85    #[serde(default)]
86    pub forbidden_actions: Vec<String>,
87}
88
89/// Intent definition -- machine-readable organizational purpose.
90///
91/// An intent encodes what an organization wants an agent to achieve,
92/// how it should handle conflicts, and what boundaries it must respect.
93#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
94#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
95pub struct IntentDef {
96    /// Unique name for this intent (e.g., "customer_success")
97    pub name: String,
98    /// Ordered list of goal names the agent should pursue
99    #[serde(default)]
100    pub goals: Vec<String>,
101    /// Rules for resolving conflicting priorities
102    #[serde(default)]
103    pub resolution_rules: Vec<ResolutionRule>,
104    /// How much autonomy the agent has
105    #[serde(default)]
106    pub autonomy_level: AutonomyLevel,
107    /// What the agent can and cannot do
108    #[serde(default)]
109    pub delegation_boundaries: DelegationBoundary,
110    /// Data points that indicate successful alignment
111    #[serde(default)]
112    pub alignment_signals: Vec<AlignmentSignal>,
113    /// Threshold below which drift alert fires (0.0-1.0)
114    #[serde(default = "default_drift_threshold")]
115    pub drift_threshold: f64,
116}
117
118fn default_drift_threshold() -> f64 {
119    0.85
120}
121
122/// Report of alignment drift detection.
123#[derive(Debug, Clone, Serialize, Deserialize)]
124#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
125pub struct DriftReport {
126    /// Current alignment score (0.0-1.0)
127    pub alignment_score: f64,
128    /// Whether drift threshold has been breached
129    pub is_drifting: bool,
130    /// Per-signal scores
131    pub signal_scores: Vec<SignalScore>,
132    /// Multi-agent drift measurement (populated when trajectory involves
133    /// multiple agents). Carries the composite divergence metric from
134    /// `DriftMeter::compute()`.
135    #[serde(default, skip_serializing_if = "Option::is_none")]
136    pub multi_agent_drift: Option<crate::drift::DriftMeter>,
137}
138
139/// Score for a single alignment signal.
140#[derive(Debug, Clone, Serialize, Deserialize)]
141#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
142pub struct SignalScore {
143    /// Name of the alignment signal
144    pub signal_name: String,
145    /// Current measured value
146    pub current_value: f64,
147    /// Whether the signal is within target
148    pub within_target: bool,
149}
150
151#[cfg(test)]
152mod tests {
153    use super::*;
154
155    #[test]
156    fn autonomy_level_default_is_operator() {
157        assert_eq!(AutonomyLevel::default(), AutonomyLevel::Operator);
158    }
159
160    #[test]
161    fn autonomy_level_serde_roundtrip() {
162        let levels = vec![
163            AutonomyLevel::Operator,
164            AutonomyLevel::Collaborator,
165            AutonomyLevel::Consultant,
166            AutonomyLevel::Approver,
167            AutonomyLevel::Observer,
168        ];
169        for level in levels {
170            let json = serde_json::to_string(&level).unwrap();
171            let d: AutonomyLevel = serde_json::from_str(&json).unwrap();
172            assert_eq!(level, d);
173        }
174    }
175
176    #[test]
177    fn signal_target_serde_roundtrip() {
178        let targets = vec![
179            SignalTarget::Above(0.8),
180            SignalTarget::Below(0.2),
181            SignalTarget::Between { min: 0.3, max: 0.7 },
182        ];
183        for t in targets {
184            let json = serde_json::to_string(&t).unwrap();
185            let d: SignalTarget = serde_json::from_str(&json).unwrap();
186            assert_eq!(t, d);
187        }
188    }
189
190    #[test]
191    fn delegation_boundary_default_is_empty() {
192        let d = DelegationBoundary::default();
193        assert!(d.authorized_actions.is_empty());
194        assert!(d.requires_approval.is_empty());
195        assert!(d.forbidden_actions.is_empty());
196    }
197
198    #[test]
199    fn intent_def_serde_roundtrip() {
200        let intent = IntentDef {
201            name: "customer_success".into(),
202            goals: vec!["retention".into(), "satisfaction".into()],
203            resolution_rules: vec![ResolutionRule {
204                condition: "sentiment < 0.3".into(),
205                priority: vec!["satisfaction".into()],
206                escalate_to: Some("manager".into()),
207                max_authority: 1000.0,
208            }],
209            autonomy_level: AutonomyLevel::Collaborator,
210            delegation_boundaries: DelegationBoundary {
211                authorized_actions: vec!["send_email".into()],
212                requires_approval: vec!["refund".into()],
213                forbidden_actions: vec!["delete_account".into()],
214            },
215            alignment_signals: vec![AlignmentSignal {
216                name: "retention_rate".into(),
217                source: "customer_events".into(),
218                metric: "30d_retention".into(),
219                target: SignalTarget::Above(0.85),
220            }],
221            drift_threshold: 0.85,
222        };
223        let json = serde_json::to_string(&intent).unwrap();
224        let d: IntentDef = serde_json::from_str(&json).unwrap();
225        assert_eq!(intent, d);
226    }
227
228    #[test]
229    fn default_drift_threshold_is_0_85() {
230        let intent: IntentDef = serde_json::from_str(
231            r#"{
232            "name": "test",
233            "goals": [],
234            "resolution_rules": [],
235            "delegation_boundaries": {}
236        }"#,
237        )
238        .unwrap();
239        assert!((intent.drift_threshold - 0.85).abs() < f64::EPSILON);
240    }
241
242    #[test]
243    fn drift_report_serde_roundtrip() {
244        let report = DriftReport {
245            alignment_score: 0.92,
246            is_drifting: false,
247            signal_scores: vec![SignalScore {
248                signal_name: "retention".into(),
249                current_value: 0.95,
250                within_target: true,
251            }],
252            multi_agent_drift: None,
253        };
254        let json = serde_json::to_string(&report).unwrap();
255        let d: DriftReport = serde_json::from_str(&json).unwrap();
256        assert!((d.alignment_score - 0.92).abs() < f64::EPSILON);
257        assert!(!d.is_drifting);
258        assert_eq!(d.signal_scores.len(), 1);
259    }
260}