lean_ctx_protocol/
control_plane.rs1use crate::{CapabilityManifestV1, ContextBundleV1, TaskComplexity, TaskEnvelopeV1};
4use serde::{Deserialize, Serialize};
5
6#[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#[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
26pub trait ControlPlaneContract {
28 fn decide(&self, request: ControlPlaneRequest) -> ControlPlaneDecision;
30}
31
32#[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}