Skip to main content

lean_ctx_protocol/
control_plane.rs

1//! Model and context-selection control-plane contracts.
2//!
3//! `ContextBundleV1` is retained only for historical wire compatibility. It
4//! does not grant the Engine Product context-selection authority.
5
6use crate::{CapabilityManifestV1, ContextBundleV1, TaskComplexity, TaskEnvelopeV1};
7use serde::{Deserialize, Serialize};
8
9/// Inputs supplied to a control plane for one task-routing decision.
10#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
11#[serde(deny_unknown_fields)]
12pub struct ControlPlaneRequest {
13    pub task_envelope: TaskEnvelopeV1,
14    /// Frozen legacy Product bundle; removal follows the knowledge-routing gate.
15    pub context_bundle: ContextBundleV1,
16    pub available_capabilities: Vec<CapabilityManifestV1>,
17}
18
19/// A selected provider, model, and resource allocation for a task.
20#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
21#[serde(deny_unknown_fields)]
22pub struct ControlPlaneDecision {
23    pub selected_model: String,
24    pub selected_provider: String,
25    pub reasoning_budget: u64,
26    pub context_adjustments: Vec<String>,
27    pub confidence: u16,
28}
29
30/// Extension point for Enterprise control planes such as `AdaptiveControlPlane`.
31pub trait ControlPlaneContract {
32    /// Select a model, provider, and deterministic execution parameters.
33    fn decide(&self, request: ControlPlaneRequest) -> ControlPlaneDecision;
34}
35
36/// OSS rule-based control plane with no remote policy dependency.
37#[derive(Debug, Default, Clone, Copy)]
38pub struct LocalControlPlane;
39
40impl ControlPlaneContract for LocalControlPlane {
41    fn decide(&self, request: ControlPlaneRequest) -> ControlPlaneDecision {
42        let selected_provider = request
43            .available_capabilities
44            .iter()
45            .map(|capability| capability.provider.as_str())
46            .min()
47            .unwrap_or("local")
48            .to_owned();
49        let reasoning_budget = match request.task_envelope.complexity {
50            TaskComplexity::Unknown | TaskComplexity::Low => 1,
51            TaskComplexity::Medium => 2,
52            TaskComplexity::High => 3,
53            TaskComplexity::Critical => 4,
54        };
55        let confidence = match request.task_envelope.complexity {
56            TaskComplexity::Unknown => 500,
57            TaskComplexity::Low => 900,
58            TaskComplexity::Medium => 800,
59            TaskComplexity::High => 700,
60            TaskComplexity::Critical => 600,
61        };
62
63        ControlPlaneDecision {
64            selected_model: request
65                .task_envelope
66                .model_policy_ref
67                .unwrap_or_else(|| "local/default".to_owned()),
68            selected_provider,
69            reasoning_budget,
70            context_adjustments: Vec::new(),
71            confidence,
72        }
73    }
74}
75
76#[cfg(test)]
77mod tests {
78    use super::*;
79    use crate::{AgentId, ProjectId, SessionId, TaskId, TraceId};
80
81    fn id<T>(value: &str) -> T
82    where
83        T: TryFrom<String>,
84        <T as TryFrom<String>>::Error: std::fmt::Debug,
85    {
86        T::try_from(value.to_owned()).expect("identifier should be valid")
87    }
88
89    fn request() -> ControlPlaneRequest {
90        ControlPlaneRequest {
91            task_envelope: TaskEnvelopeV1 {
92                schema_version: 1,
93                task_id: id::<TaskId>("task-1"),
94                trace_id: id::<TraceId>("trace-1"),
95                project_id: id::<ProjectId>("project-1"),
96                session_id: id::<SessionId>("session-1"),
97                agent_id: id::<AgentId>("agent-1"),
98                complexity: TaskComplexity::Medium,
99                created_at: "2026-08-12T00:00:00Z".to_owned(),
100                parent_task_id: None,
101                tenant_id: None,
102                intent: None,
103                task_class: None,
104                risk_class: None,
105                quality_requirement_milli: None,
106                cost_budget_micros: None,
107                latency_budget_ms: None,
108                data_classification: None,
109                region_policy_ref: None,
110                model_policy_ref: None,
111                context_state_ref: None,
112                outcome_contract_ref: None,
113                extensions: Default::default(),
114            },
115            context_bundle: ContextBundleV1::default(),
116            available_capabilities: Vec::new(),
117        }
118    }
119
120    #[test]
121    fn local_control_plane_is_deterministic() {
122        let control_plane = LocalControlPlane;
123        assert_eq!(
124            control_plane.decide(request()),
125            control_plane.decide(request())
126        );
127    }
128
129    #[test]
130    fn contract_is_object_safe() {
131        let control_plane: &dyn ControlPlaneContract = &LocalControlPlane;
132        assert_eq!(control_plane.decide(request()).selected_provider, "local");
133    }
134}