Skip to main content

lean_ctx_protocol/
control_plane.rs

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