lean_ctx_protocol/
control_plane.rs1use crate::{CapabilityManifestV1, ContextBundleV1, TaskComplexity, TaskEnvelopeV1};
7use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
11#[serde(deny_unknown_fields)]
12pub struct ControlPlaneRequest {
13 pub task_envelope: TaskEnvelopeV1,
14 pub context_bundle: ContextBundleV1,
16 pub available_capabilities: Vec<CapabilityManifestV1>,
17}
18
19#[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
30pub trait ControlPlaneContract {
32 fn decide(&self, request: ControlPlaneRequest) -> ControlPlaneDecision;
34}
35
36#[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}