Skip to main content

lean_ctx_protocol/
outcome_engine.rs

1//! Outcome-learning contracts for evaluating control-plane decisions.
2
3use crate::{AcceptanceState, ControlPlaneDecision, OutcomeSignalsV1, TaskId, TaskProfileV1};
4use serde::{Deserialize, Serialize};
5
6/// Quality dimensions reported alongside an observed task outcome.
7#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
8#[serde(deny_unknown_fields)]
9pub struct QualityDimensionsV2 {
10    pub correctness_milli: u16,
11    pub completeness_milli: u16,
12    pub maintainability_milli: u16,
13    pub efficiency_milli: u16,
14}
15
16/// Rich task outcome signal used by the learning engine.
17#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
18#[serde(deny_unknown_fields)]
19pub struct OutcomeSignalV2 {
20    pub task_id: TaskId,
21    pub accepted: AcceptanceState,
22    pub quality_score_milli: u16,
23    pub quality_dimensions: QualityDimensionsV2,
24    pub signals: OutcomeSignalsV1,
25    pub observed_at: String,
26}
27
28/// One decision/outcome pair available for adaptive learning.
29#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
30#[serde(deny_unknown_fields)]
31pub struct LearningEvent {
32    pub task_id: TaskId,
33    pub decision_taken: ControlPlaneDecision,
34    pub outcome_observed: OutcomeSignalV2,
35    pub counterfactual_estimate: Option<f64>,
36}
37
38/// Extension point for Enterprise outcome engines such as `AdaptiveOutcomeEngine`.
39pub trait OutcomeEngineContract {
40    /// Record an observed outcome for a prior decision.
41    fn observe(&mut self, event: LearningEvent);
42
43    /// Estimate an outcome score for a task profile in the inclusive 0.0..=1.0 range.
44    fn predict_outcome(&self, task_profile: &TaskProfileV1) -> f64;
45}
46
47/// OSS outcome engine that retains observations without producing predictions.
48#[derive(Debug, Default, Clone, PartialEq)]
49pub struct LocalOutcomeEngine {
50    events: Vec<LearningEvent>,
51}
52
53impl LocalOutcomeEngine {
54    /// Return the observations retained by this local process.
55    pub fn events(&self) -> &[LearningEvent] {
56        &self.events
57    }
58}
59
60impl OutcomeEngineContract for LocalOutcomeEngine {
61    fn observe(&mut self, event: LearningEvent) {
62        self.events.push(event);
63    }
64
65    fn predict_outcome(&self, _task_profile: &TaskProfileV1) -> f64 {
66        0.0
67    }
68}
69
70#[cfg(test)]
71mod tests {
72    use super::*;
73
74    fn id<T>(value: &str) -> T
75    where
76        T: TryFrom<String>,
77        <T as TryFrom<String>>::Error: std::fmt::Debug,
78    {
79        T::try_from(value.to_owned()).expect("identifier should be valid")
80    }
81
82    fn event() -> LearningEvent {
83        LearningEvent {
84            task_id: id("task-1"),
85            decision_taken: ControlPlaneDecision {
86                selected_model: "local/default".to_owned(),
87                selected_provider: "local".to_owned(),
88                reasoning_budget: 1,
89                context_adjustments: Vec::new(),
90                confidence: 900,
91            },
92            outcome_observed: OutcomeSignalV2 {
93                task_id: id("task-1"),
94                accepted: AcceptanceState::Accepted,
95                quality_score_milli: 900,
96                quality_dimensions: QualityDimensionsV2 {
97                    correctness_milli: 900,
98                    completeness_milli: 900,
99                    maintainability_milli: 900,
100                    efficiency_milli: 900,
101                },
102                signals: OutcomeSignalsV1 {
103                    build: None,
104                    tests: None,
105                    lint: None,
106                    typecheck: None,
107                    completion: None,
108                    pr: None,
109                    correction: None,
110                    rollback: None,
111                    retry: None,
112                },
113                observed_at: "2026-08-12T00:00:00Z".to_owned(),
114            },
115            counterfactual_estimate: None,
116        }
117    }
118
119    #[test]
120    fn local_outcome_engine_stores_events_without_prediction() {
121        let mut engine = LocalOutcomeEngine::default();
122        engine.observe(event());
123        assert_eq!(engine.events().len(), 1);
124        assert_eq!(engine.predict_outcome(&TaskProfileV1::default()), 0.0);
125    }
126
127    #[test]
128    fn contract_is_object_safe() {
129        let mut engine: Box<dyn OutcomeEngineContract> = Box::new(LocalOutcomeEngine::default());
130        engine.observe(event());
131        assert_eq!(engine.predict_outcome(&TaskProfileV1::default()), 0.0);
132    }
133}