1use serde::{Deserialize, Serialize};
30use std::collections::HashMap;
31
32#[derive(Debug, Clone, Serialize, Deserialize)]
41pub struct KernelIntent {
42 pub task: String,
44 pub criteria: Vec<String>,
46 pub max_tokens: usize,
48}
49
50impl KernelIntent {
51 #[must_use]
53 pub fn new(task: impl Into<String>) -> Self {
54 Self {
55 task: task.into(),
56 criteria: Vec::new(),
57 max_tokens: 1024,
58 }
59 }
60
61 #[must_use]
63 pub fn with_criteria(mut self, criteria: impl Into<String>) -> Self {
64 self.criteria.push(criteria.into());
65 self
66 }
67
68 #[must_use]
70 pub fn with_max_tokens(mut self, max_tokens: usize) -> Self {
71 self.max_tokens = max_tokens;
72 self
73 }
74}
75
76#[derive(Debug, Clone, Serialize, Deserialize)]
81pub struct KernelContext {
82 pub state: HashMap<String, serde_json::Value>,
84 pub facts: Vec<ContextFact>,
86 pub tenant_id: Option<String>,
88}
89
90impl KernelContext {
91 #[must_use]
93 pub fn new() -> Self {
94 Self {
95 state: HashMap::new(),
96 facts: Vec::new(),
97 tenant_id: None,
98 }
99 }
100
101 #[must_use]
103 pub fn with_state(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
104 self.state.insert(key.into(), value);
105 self
106 }
107
108 #[must_use]
110 pub fn with_fact(
111 mut self,
112 key: impl Into<String>,
113 id: impl Into<String>,
114 content: impl Into<String>,
115 ) -> Self {
116 self.facts.push(ContextFact {
117 key: key.into(),
118 id: id.into(),
119 content: content.into(),
120 });
121 self
122 }
123
124 #[must_use]
126 pub fn with_tenant(mut self, tenant_id: impl Into<String>) -> Self {
127 self.tenant_id = Some(tenant_id.into());
128 self
129 }
130}
131
132impl Default for KernelContext {
133 fn default() -> Self {
134 Self::new()
135 }
136}
137
138#[derive(Debug, Clone, Serialize, Deserialize)]
143pub struct ContextFact {
144 pub key: String,
146 pub id: String,
148 pub content: String,
150}
151
152#[derive(Debug, Clone, Serialize, Deserialize)]
162pub struct KernelPolicy {
163 pub adapter_id: Option<String>,
165 pub recall_enabled: bool,
167 pub recall_max_candidates: usize,
169 pub recall_min_score: f32,
171 pub seed: Option<u64>,
173 pub requires_human: bool,
175 pub required_truths: Vec<String>,
177}
178
179impl KernelPolicy {
180 #[must_use]
182 pub fn new() -> Self {
183 Self {
184 adapter_id: None,
185 recall_enabled: false,
186 recall_max_candidates: 5,
187 recall_min_score: 0.7,
188 seed: None,
189 requires_human: false,
190 required_truths: Vec::new(),
191 }
192 }
193
194 #[must_use]
196 pub fn deterministic(seed: u64) -> Self {
197 Self {
198 seed: Some(seed),
199 ..Self::new()
200 }
201 }
202
203 #[must_use]
205 pub fn with_adapter(mut self, adapter_id: impl Into<String>) -> Self {
206 self.adapter_id = Some(adapter_id.into());
207 self
208 }
209
210 #[must_use]
212 pub fn with_recall(mut self, enabled: bool) -> Self {
213 self.recall_enabled = enabled;
214 self
215 }
216
217 #[must_use]
219 pub fn with_human_required(mut self) -> Self {
220 self.requires_human = true;
221 self
222 }
223
224 #[must_use]
226 pub fn with_required_truth(mut self, truth: impl Into<String>) -> Self {
227 self.required_truths.push(truth.into());
228 self
229 }
230}
231
232impl Default for KernelPolicy {
233 fn default() -> Self {
234 Self::new()
235 }
236}
237
238#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
247pub enum RiskTier {
248 Low,
249 Medium,
250 High,
251 Critical,
252}
253
254#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
259pub enum DataClassification {
260 Public,
261 Internal,
262 Confidential,
263 Restricted,
264}
265
266#[derive(Debug, Clone, Serialize, Deserialize)]
279pub struct RoutingPolicy {
280 pub truth_preferences: HashMap<String, String>,
282 pub risk_tier_backends: HashMap<RiskTier, Vec<String>>,
284 pub data_classification_backends: HashMap<DataClassification, Vec<String>>,
286 pub default_backend: String,
288}
289
290impl Default for RoutingPolicy {
291 fn default() -> Self {
292 Self {
293 truth_preferences: HashMap::new(),
294 risk_tier_backends: HashMap::new(),
295 data_classification_backends: HashMap::new(),
296 default_backend: "local".to_string(),
297 }
298 }
299}
300
301impl RoutingPolicy {
302 #[must_use]
307 pub fn default_deny_remote() -> Self {
308 let mut policy = Self::default();
309 policy
311 .risk_tier_backends
312 .insert(RiskTier::Critical, vec!["local".to_string()]);
313 policy
314 .risk_tier_backends
315 .insert(RiskTier::High, vec!["local".to_string()]);
316 policy
317 .data_classification_backends
318 .insert(DataClassification::Restricted, vec!["local".to_string()]);
319 policy
320 .data_classification_backends
321 .insert(DataClassification::Confidential, vec!["local".to_string()]);
322 policy
323 }
324
325 #[must_use]
327 pub fn is_backend_allowed(
328 &self,
329 backend_name: &str,
330 risk_tier: RiskTier,
331 data_classification: DataClassification,
332 ) -> bool {
333 if let Some(allowed) = self.risk_tier_backends.get(&risk_tier) {
335 if !allowed.contains(&backend_name.to_string()) && !allowed.is_empty() {
336 return false;
337 }
338 }
339
340 if let Some(allowed) = self.data_classification_backends.get(&data_classification) {
342 if !allowed.contains(&backend_name.to_string()) && !allowed.is_empty() {
343 return false;
344 }
345 }
346
347 true
348 }
349
350 #[must_use]
352 pub fn select_backend(
353 &self,
354 truth_ids: &[String],
355 risk_tier: RiskTier,
356 data_classification: DataClassification,
357 ) -> &str {
358 for truth_id in truth_ids {
360 if let Some(backend) = self.truth_preferences.get(truth_id) {
361 return backend;
362 }
363 }
364
365 if let Some(backends) = self.risk_tier_backends.get(&risk_tier) {
367 if let Some(backend) = backends.first() {
368 return backend;
369 }
370 }
371
372 if let Some(backends) = self.data_classification_backends.get(&data_classification) {
374 if let Some(backend) = backends.first() {
375 return backend;
376 }
377 }
378
379 &self.default_backend
381 }
382}
383
384#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
394pub enum DecisionStep {
395 Reasoning,
397 Evaluation,
399 Planning,
401}
402
403impl DecisionStep {
404 #[must_use]
406 pub fn expected_contract(&self) -> &'static str {
407 match self {
408 Self::Reasoning => "Reasoning",
409 Self::Evaluation => "Evaluation",
410 Self::Planning => "Planning",
411 }
412 }
413
414 #[must_use]
416 pub fn as_str(&self) -> &'static str {
417 match self {
418 Self::Reasoning => "reasoning",
419 Self::Evaluation => "evaluation",
420 Self::Planning => "planning",
421 }
422 }
423}
424
425impl Default for DecisionStep {
426 fn default() -> Self {
427 Self::Reasoning
428 }
429}
430
431#[derive(Debug, Clone, Serialize, Deserialize)]
440#[serde(tag = "type")]
441pub enum TraceLink {
442 Local(LocalTraceLink),
444 Remote(RemoteTraceLink),
446}
447
448impl TraceLink {
449 #[must_use]
451 pub fn is_replay_eligible(&self) -> bool {
452 matches!(self, TraceLink::Local(_))
453 }
454
455 #[must_use]
457 pub fn replayability(&self) -> Replayability {
458 match self {
459 TraceLink::Local(_) => Replayability::Deterministic,
460 TraceLink::Remote(r) => r.replayability,
461 }
462 }
463}
464
465#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
469pub enum Replayability {
470 Deterministic,
472 BestEffort,
474 None,
476}
477
478impl Default for Replayability {
479 fn default() -> Self {
480 Self::None
481 }
482}
483
484#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
495pub enum ReplayabilityDowngradeReason {
496 RecallEmbedderNotDeterministic,
498 RecallCorpusNotContentAddressed,
500 RemoteBackendUsed,
502 NoSeedProvided,
504 MultipleReasons,
506}
507
508#[derive(Debug, Clone, Serialize, Deserialize)]
513pub struct LocalTraceLink {
514 pub base_model_hash: String,
516 pub adapter: Option<AdapterTrace>,
518 pub tokenizer_hash: String,
520 pub seed: u64,
522 pub sampler: SamplerParams,
524 pub prompt_version: String,
526 pub recall: Option<RecallTrace>,
528 pub weights_mutated: bool,
530 pub execution_env: ExecutionEnv,
532}
533
534#[derive(Debug, Clone, Serialize, Deserialize)]
536pub struct AdapterTrace {
537 pub adapter_id: String,
538 pub adapter_hash: String,
539 pub merged: bool,
540}
541
542#[derive(Debug, Clone, Serialize, Deserialize)]
544pub struct SamplerParams {
545 pub temperature: f32,
546 pub top_p: f32,
547 pub top_k: Option<usize>,
548}
549
550impl Default for SamplerParams {
551 fn default() -> Self {
552 Self {
553 temperature: 0.0,
554 top_p: 1.0,
555 top_k: None,
556 }
557 }
558}
559
560#[derive(Debug, Clone, Serialize, Deserialize)]
562pub struct RecallTrace {
563 pub corpus_fingerprint: String,
564 pub candidate_ids: Vec<String>,
565 pub candidate_scores: Vec<f32>,
566 pub injected_count: usize,
567}
568
569#[derive(Debug, Clone, Serialize, Deserialize)]
571pub struct ExecutionEnv {
572 pub device: String,
573 pub backend: String,
574 pub precision: String,
575}
576
577impl Default for ExecutionEnv {
578 fn default() -> Self {
579 Self {
580 device: "cpu".to_string(),
581 backend: "ndarray".to_string(),
582 precision: "f32".to_string(),
583 }
584 }
585}
586
587#[derive(Debug, Clone, Serialize, Deserialize)]
592pub struct RemoteTraceLink {
593 pub provider_name: String,
595 pub provider_model_id: String,
597 pub request_fingerprint: String,
599 pub response_fingerprint: String,
601 pub temperature: f32,
603 pub top_p: f32,
605 pub max_tokens: usize,
607 pub provider_metadata: HashMap<String, String>,
609 pub retried: bool,
611 pub retry_reasons: Vec<String>,
613 pub replayability: Replayability,
615}
616
617#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
626pub enum ProposalKind {
627 Claims,
629 Plan,
631 Classification,
633 Evaluation,
635 DraftDocument,
637 Reasoning,
639}
640
641impl Default for ProposalKind {
642 fn default() -> Self {
643 Self::Reasoning
644 }
645}
646
647#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
649pub enum ContentKind {
650 Claim,
651 Plan,
652 Classification,
653 Evaluation,
654 Draft,
655 Reasoning,
656}
657
658impl Default for ContentKind {
659 fn default() -> Self {
660 Self::Reasoning
661 }
662}
663
664impl From<ContentKind> for ProposalKind {
665 fn from(kind: ContentKind) -> Self {
666 match kind {
667 ContentKind::Claim => ProposalKind::Claims,
668 ContentKind::Plan => ProposalKind::Plan,
669 ContentKind::Classification => ProposalKind::Classification,
670 ContentKind::Evaluation => ProposalKind::Evaluation,
671 ContentKind::Draft => ProposalKind::DraftDocument,
672 ContentKind::Reasoning => ProposalKind::Reasoning,
673 }
674 }
675}
676
677#[derive(Debug, Clone, Serialize, Deserialize)]
682pub struct ProposedContent {
683 pub id: String,
685 pub kind: ContentKind,
687 pub content: String,
689 pub structured: Option<serde_json::Value>,
691 pub confidence: Option<f32>,
693 pub requires_human: bool,
695}
696
697impl ProposedContent {
698 #[must_use]
700 pub fn new(id: impl Into<String>, kind: ContentKind, content: impl Into<String>) -> Self {
701 Self {
702 id: id.into(),
703 kind,
704 content: content.into(),
705 structured: None,
706 confidence: None,
707 requires_human: false,
708 }
709 }
710
711 #[must_use]
713 pub fn with_human_required(mut self) -> Self {
714 self.requires_human = true;
715 self
716 }
717
718 #[must_use]
720 pub fn with_confidence(mut self, confidence: f32) -> Self {
721 self.confidence = Some(confidence);
722 self
723 }
724}
725
726#[derive(Debug, Clone, Serialize, Deserialize)]
728pub struct ContractResult {
729 pub name: String,
731 pub passed: bool,
733 pub failure_reason: Option<String>,
735}
736
737impl ContractResult {
738 #[must_use]
740 pub fn passed(name: impl Into<String>) -> Self {
741 Self {
742 name: name.into(),
743 passed: true,
744 failure_reason: None,
745 }
746 }
747
748 #[must_use]
750 pub fn failed(name: impl Into<String>, reason: impl Into<String>) -> Self {
751 Self {
752 name: name.into(),
753 passed: false,
754 failure_reason: Some(reason.into()),
755 }
756 }
757}
758
759#[derive(Debug, Clone, Serialize, Deserialize)]
770pub struct KernelProposal {
771 pub id: String,
773 pub kind: ProposalKind,
775 pub payload: String,
777 pub structured_payload: Option<serde_json::Value>,
779 pub trace_link: TraceLink,
781 pub contract_results: Vec<ContractResult>,
783 pub requires_human: bool,
785 pub confidence: Option<f32>,
787}
788
789impl KernelProposal {
790 #[must_use]
792 pub fn all_contracts_passed(&self) -> bool {
793 self.contract_results.iter().all(|r| r.passed)
794 }
795
796 #[must_use]
798 pub fn failed_contracts(&self) -> Vec<&str> {
799 self.contract_results
800 .iter()
801 .filter(|r| !r.passed)
802 .map(|r| r.name.as_str())
803 .collect()
804 }
805
806 #[must_use]
812 pub fn is_auto_promotable(&self) -> bool {
813 self.all_contracts_passed() && !self.requires_human
814 }
815}
816
817#[cfg(test)]
818mod tests {
819 use super::*;
820
821 #[test]
822 fn trace_link_replayability() {
823 let local = TraceLink::Local(LocalTraceLink {
824 base_model_hash: "abc123".to_string(),
825 adapter: None,
826 tokenizer_hash: "tok123".to_string(),
827 seed: 42,
828 sampler: SamplerParams::default(),
829 prompt_version: "v1".to_string(),
830 recall: None,
831 weights_mutated: false,
832 execution_env: ExecutionEnv::default(),
833 });
834
835 let remote = TraceLink::Remote(RemoteTraceLink {
836 provider_name: "anthropic".to_string(),
837 provider_model_id: "claude-3-opus".to_string(),
838 request_fingerprint: "req123".to_string(),
839 response_fingerprint: "resp456".to_string(),
840 temperature: 0.0,
841 top_p: 1.0,
842 max_tokens: 1024,
843 provider_metadata: HashMap::new(),
844 retried: false,
845 retry_reasons: vec![],
846 replayability: Replayability::BestEffort,
847 });
848
849 assert!(local.is_replay_eligible());
850 assert!(!remote.is_replay_eligible());
851
852 assert_eq!(local.replayability(), Replayability::Deterministic);
853 assert_eq!(remote.replayability(), Replayability::BestEffort);
854 }
855
856 #[test]
857 fn proposal_kind_conversion() {
858 assert_eq!(ProposalKind::from(ContentKind::Claim), ProposalKind::Claims);
859 assert_eq!(ProposalKind::from(ContentKind::Plan), ProposalKind::Plan);
860 assert_eq!(
861 ProposalKind::from(ContentKind::Reasoning),
862 ProposalKind::Reasoning
863 );
864 }
865
866 #[test]
867 fn contract_result_helpers() {
868 let passed = ContractResult::passed("grounded-answering");
869 assert!(passed.passed);
870 assert!(passed.failure_reason.is_none());
871
872 let failed = ContractResult::failed("reasoning", "missing CONCLUSION");
873 assert!(!failed.passed);
874 assert_eq!(failed.failure_reason.as_deref(), Some("missing CONCLUSION"));
875 }
876
877 #[test]
878 fn kernel_proposal_auto_promotable() {
879 let local_trace = TraceLink::Local(LocalTraceLink {
880 base_model_hash: "hash".to_string(),
881 adapter: None,
882 tokenizer_hash: "tok".to_string(),
883 seed: 1,
884 sampler: SamplerParams::default(),
885 prompt_version: "v1".to_string(),
886 recall: None,
887 weights_mutated: false,
888 execution_env: ExecutionEnv::default(),
889 });
890
891 let promotable = KernelProposal {
893 id: "p1".to_string(),
894 kind: ProposalKind::Claims,
895 payload: "claim".to_string(),
896 structured_payload: None,
897 trace_link: local_trace.clone(),
898 contract_results: vec![ContractResult::passed("c1")],
899 requires_human: false,
900 confidence: Some(0.9),
901 };
902 assert!(promotable.is_auto_promotable());
903
904 let needs_human = KernelProposal {
906 id: "p2".to_string(),
907 kind: ProposalKind::Claims,
908 payload: "claim".to_string(),
909 structured_payload: None,
910 trace_link: local_trace.clone(),
911 contract_results: vec![ContractResult::passed("c1")],
912 requires_human: true,
913 confidence: Some(0.9),
914 };
915 assert!(!needs_human.is_auto_promotable());
916
917 let failed_contract = KernelProposal {
919 id: "p3".to_string(),
920 kind: ProposalKind::Claims,
921 payload: "claim".to_string(),
922 structured_payload: None,
923 trace_link: local_trace,
924 contract_results: vec![ContractResult::failed("c1", "reason")],
925 requires_human: false,
926 confidence: Some(0.9),
927 };
928 assert!(!failed_contract.is_auto_promotable());
929 }
930
931 #[test]
936 fn kernel_intent_builder() {
937 let intent = KernelIntent::new("analyze_metrics")
938 .with_criteria("find anomalies")
939 .with_criteria("suggest fixes")
940 .with_max_tokens(512);
941
942 assert_eq!(intent.task, "analyze_metrics");
943 assert_eq!(intent.criteria.len(), 2);
944 assert_eq!(intent.criteria[0], "find anomalies");
945 assert_eq!(intent.criteria[1], "suggest fixes");
946 assert_eq!(intent.max_tokens, 512);
947 }
948
949 #[test]
950 fn kernel_context_builder() {
951 let context = KernelContext::new()
952 .with_state("metric", serde_json::json!(0.5))
953 .with_fact("Seeds", "seed-1", "Some seed fact")
954 .with_tenant("tenant-123");
955
956 assert!(context.state.contains_key("metric"));
957 assert_eq!(context.facts.len(), 1);
958 assert_eq!(context.facts[0].key, "Seeds");
959 assert_eq!(context.facts[0].id, "seed-1");
960 assert_eq!(context.tenant_id, Some("tenant-123".to_string()));
961 }
962
963 #[test]
964 fn kernel_context_default() {
965 let context = KernelContext::default();
966 assert!(context.state.is_empty());
967 assert!(context.facts.is_empty());
968 assert!(context.tenant_id.is_none());
969 }
970
971 #[test]
972 fn kernel_policy_default() {
973 let policy = KernelPolicy::default();
974 assert!(policy.adapter_id.is_none());
975 assert!(!policy.recall_enabled);
976 assert_eq!(policy.recall_max_candidates, 5);
977 assert!((policy.recall_min_score - 0.7).abs() < f32::EPSILON);
978 assert!(policy.seed.is_none());
979 assert!(!policy.requires_human);
980 assert!(policy.required_truths.is_empty());
981 }
982
983 #[test]
984 fn kernel_policy_deterministic() {
985 let policy = KernelPolicy::deterministic(42)
986 .with_adapter("llm/grounded@1.0.0")
987 .with_recall(true)
988 .with_human_required()
989 .with_required_truth("grounded-answering");
990
991 assert_eq!(policy.seed, Some(42));
992 assert_eq!(policy.adapter_id, Some("llm/grounded@1.0.0".to_string()));
993 assert!(policy.recall_enabled);
994 assert!(policy.requires_human);
995 assert_eq!(policy.required_truths, vec!["grounded-answering"]);
996 }
997}