1use serde::{Deserialize, Serialize};
27use std::collections::HashMap;
28
29#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct KernelIntent {
39 pub task: String,
41 pub criteria: Vec<String>,
43 pub max_tokens: usize,
45}
46
47impl KernelIntent {
48 #[must_use]
50 pub fn new(task: impl Into<String>) -> Self {
51 Self {
52 task: task.into(),
53 criteria: Vec::new(),
54 max_tokens: 1024,
55 }
56 }
57
58 #[must_use]
60 pub fn with_criteria(mut self, criteria: impl Into<String>) -> Self {
61 self.criteria.push(criteria.into());
62 self
63 }
64
65 #[must_use]
67 pub fn with_max_tokens(mut self, max_tokens: usize) -> Self {
68 self.max_tokens = max_tokens;
69 self
70 }
71}
72
73#[derive(Debug, Clone, Serialize, Deserialize)]
78pub struct KernelContext {
79 pub state: HashMap<String, serde_json::Value>,
81 pub facts: Vec<ContextFact>,
83 pub tenant_id: Option<String>,
85}
86
87impl KernelContext {
88 #[must_use]
90 pub fn new() -> Self {
91 Self {
92 state: HashMap::new(),
93 facts: Vec::new(),
94 tenant_id: None,
95 }
96 }
97
98 #[must_use]
100 pub fn with_state(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
101 self.state.insert(key.into(), value);
102 self
103 }
104
105 #[must_use]
107 pub fn with_fact(
108 mut self,
109 key: impl Into<String>,
110 id: impl Into<String>,
111 content: impl Into<String>,
112 ) -> Self {
113 self.facts.push(ContextFact {
114 key: key.into(),
115 id: id.into(),
116 content: content.into(),
117 });
118 self
119 }
120
121 #[must_use]
123 pub fn with_tenant(mut self, tenant_id: impl Into<String>) -> Self {
124 self.tenant_id = Some(tenant_id.into());
125 self
126 }
127}
128
129impl Default for KernelContext {
130 fn default() -> Self {
131 Self::new()
132 }
133}
134
135#[derive(Debug, Clone, Serialize, Deserialize)]
140pub struct ContextFact {
141 pub key: String,
143 pub id: String,
145 pub content: String,
147}
148
149#[derive(Debug, Clone, Serialize, Deserialize)]
159pub struct KernelPolicy {
160 pub adapter_id: Option<String>,
162 pub recall_enabled: bool,
164 pub recall_max_candidates: usize,
166 pub recall_min_score: f32,
168 pub seed: Option<u64>,
170 pub requires_human: bool,
172 pub required_truths: Vec<String>,
174}
175
176impl KernelPolicy {
177 #[must_use]
179 pub fn new() -> Self {
180 Self {
181 adapter_id: None,
182 recall_enabled: false,
183 recall_max_candidates: 5,
184 recall_min_score: 0.7,
185 seed: None,
186 requires_human: false,
187 required_truths: Vec::new(),
188 }
189 }
190
191 #[must_use]
193 pub fn deterministic(seed: u64) -> Self {
194 Self {
195 seed: Some(seed),
196 ..Self::new()
197 }
198 }
199
200 #[must_use]
202 pub fn with_adapter(mut self, adapter_id: impl Into<String>) -> Self {
203 self.adapter_id = Some(adapter_id.into());
204 self
205 }
206
207 #[must_use]
209 pub fn with_recall(mut self, enabled: bool) -> Self {
210 self.recall_enabled = enabled;
211 self
212 }
213
214 #[must_use]
216 pub fn with_human_required(mut self) -> Self {
217 self.requires_human = true;
218 self
219 }
220
221 #[must_use]
223 pub fn with_required_truth(mut self, truth: impl Into<String>) -> Self {
224 self.required_truths.push(truth.into());
225 self
226 }
227}
228
229impl Default for KernelPolicy {
230 fn default() -> Self {
231 Self::new()
232 }
233}
234
235#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
244pub enum RiskTier {
245 Low,
246 Medium,
247 High,
248 Critical,
249}
250
251#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
256pub enum DataClassification {
257 Public,
258 Internal,
259 Confidential,
260 Restricted,
261}
262
263#[derive(Debug, Clone, Serialize, Deserialize)]
276pub struct RoutingPolicy {
277 pub truth_preferences: HashMap<String, String>,
279 pub risk_tier_backends: HashMap<RiskTier, Vec<String>>,
281 pub data_classification_backends: HashMap<DataClassification, Vec<String>>,
283 pub default_backend: String,
285}
286
287impl Default for RoutingPolicy {
288 fn default() -> Self {
289 Self {
290 truth_preferences: HashMap::new(),
291 risk_tier_backends: HashMap::new(),
292 data_classification_backends: HashMap::new(),
293 default_backend: "local".to_string(),
294 }
295 }
296}
297
298impl RoutingPolicy {
299 #[must_use]
304 pub fn default_deny_remote() -> Self {
305 let mut policy = Self::default();
306 policy
308 .risk_tier_backends
309 .insert(RiskTier::Critical, vec!["local".to_string()]);
310 policy
311 .risk_tier_backends
312 .insert(RiskTier::High, vec!["local".to_string()]);
313 policy
314 .data_classification_backends
315 .insert(DataClassification::Restricted, vec!["local".to_string()]);
316 policy
317 .data_classification_backends
318 .insert(DataClassification::Confidential, vec!["local".to_string()]);
319 policy
320 }
321
322 #[must_use]
324 pub fn is_backend_allowed(
325 &self,
326 backend_name: &str,
327 risk_tier: RiskTier,
328 data_classification: DataClassification,
329 ) -> bool {
330 if let Some(allowed) = self.risk_tier_backends.get(&risk_tier) {
332 if !allowed.contains(&backend_name.to_string()) && !allowed.is_empty() {
333 return false;
334 }
335 }
336
337 if let Some(allowed) = self.data_classification_backends.get(&data_classification) {
339 if !allowed.contains(&backend_name.to_string()) && !allowed.is_empty() {
340 return false;
341 }
342 }
343
344 true
345 }
346
347 #[must_use]
349 pub fn select_backend(
350 &self,
351 truth_ids: &[String],
352 risk_tier: RiskTier,
353 data_classification: DataClassification,
354 ) -> &str {
355 for truth_id in truth_ids {
357 if let Some(backend) = self.truth_preferences.get(truth_id) {
358 return backend;
359 }
360 }
361
362 if let Some(backends) = self.risk_tier_backends.get(&risk_tier) {
364 if let Some(backend) = backends.first() {
365 return backend;
366 }
367 }
368
369 if let Some(backends) = self.data_classification_backends.get(&data_classification) {
371 if let Some(backend) = backends.first() {
372 return backend;
373 }
374 }
375
376 &self.default_backend
378 }
379}
380
381#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
391pub enum DecisionStep {
392 Reasoning,
394 Evaluation,
396 Planning,
398}
399
400impl DecisionStep {
401 #[must_use]
403 pub fn expected_contract(&self) -> &'static str {
404 match self {
405 Self::Reasoning => "Reasoning",
406 Self::Evaluation => "Evaluation",
407 Self::Planning => "Planning",
408 }
409 }
410
411 #[must_use]
413 pub fn as_str(&self) -> &'static str {
414 match self {
415 Self::Reasoning => "reasoning",
416 Self::Evaluation => "evaluation",
417 Self::Planning => "planning",
418 }
419 }
420}
421
422impl Default for DecisionStep {
423 fn default() -> Self {
424 Self::Reasoning
425 }
426}
427
428#[derive(Debug, Clone, Serialize, Deserialize)]
437#[serde(tag = "type")]
438pub enum ReplayTrace {
439 Local(LocalReplayTrace),
441 Remote(RemoteReplayTrace),
443}
444
445impl ReplayTrace {
446 #[must_use]
448 pub fn is_replay_eligible(&self) -> bool {
449 matches!(self, ReplayTrace::Local(_))
450 }
451
452 #[must_use]
454 pub fn replayability(&self) -> Replayability {
455 match self {
456 ReplayTrace::Local(_) => Replayability::Deterministic,
457 ReplayTrace::Remote(r) => r.replayability,
458 }
459 }
460}
461
462#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
466pub enum Replayability {
467 Deterministic,
469 BestEffort,
471 None,
473}
474
475impl Default for Replayability {
476 fn default() -> Self {
477 Self::None
478 }
479}
480
481#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
492pub enum ReplayabilityDowngradeReason {
493 RecallEmbedderNotDeterministic,
495 RecallCorpusNotContentAddressed,
497 RemoteBackendUsed,
499 NoSeedProvided,
501 MultipleReasons,
503}
504
505#[derive(Debug, Clone, Serialize, Deserialize)]
510pub struct LocalReplayTrace {
511 pub base_model_hash: String,
513 pub adapter: Option<AdapterTrace>,
515 pub tokenizer_hash: String,
517 pub seed: u64,
519 pub sampler: SamplerParams,
521 pub prompt_version: String,
523 pub recall: Option<RecallTrace>,
525 pub weights_mutated: bool,
527 pub execution_env: ExecutionEnv,
529}
530
531#[derive(Debug, Clone, Serialize, Deserialize)]
533pub struct AdapterTrace {
534 pub adapter_id: String,
535 pub adapter_hash: String,
536 pub merged: bool,
537}
538
539#[derive(Debug, Clone, Serialize, Deserialize)]
541pub struct SamplerParams {
542 pub temperature: f32,
543 pub top_p: f32,
544 pub top_k: Option<usize>,
545}
546
547impl Default for SamplerParams {
548 fn default() -> Self {
549 Self {
550 temperature: 0.0,
551 top_p: 1.0,
552 top_k: None,
553 }
554 }
555}
556
557#[derive(Debug, Clone, Serialize, Deserialize)]
559pub struct RecallTrace {
560 pub corpus_fingerprint: String,
561 pub candidate_ids: Vec<String>,
562 pub candidate_scores: Vec<f32>,
563 pub injected_count: usize,
564}
565
566#[derive(Debug, Clone, Serialize, Deserialize)]
568pub struct ExecutionEnv {
569 pub device: String,
570 pub backend: String,
571 pub precision: String,
572}
573
574impl Default for ExecutionEnv {
575 fn default() -> Self {
576 Self {
577 device: "cpu".to_string(),
578 backend: "ndarray".to_string(),
579 precision: "f32".to_string(),
580 }
581 }
582}
583
584#[derive(Debug, Clone, Serialize, Deserialize)]
589pub struct RemoteReplayTrace {
590 pub provider_name: String,
592 pub provider_model_id: String,
594 pub request_fingerprint: String,
596 pub response_fingerprint: String,
598 pub temperature: f32,
600 pub top_p: f32,
602 pub max_tokens: usize,
604 pub provider_metadata: HashMap<String, String>,
606 pub retried: bool,
608 pub retry_reasons: Vec<String>,
610 pub replayability: Replayability,
612}
613
614#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
623pub enum ProposalKind {
624 Claims,
626 Plan,
628 Classification,
630 Evaluation,
632 DraftDocument,
634 Reasoning,
636}
637
638impl Default for ProposalKind {
639 fn default() -> Self {
640 Self::Reasoning
641 }
642}
643
644#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
646pub enum ContentKind {
647 Claim,
648 Plan,
649 Classification,
650 Evaluation,
651 Draft,
652 Reasoning,
653}
654
655impl Default for ContentKind {
656 fn default() -> Self {
657 Self::Reasoning
658 }
659}
660
661impl From<ContentKind> for ProposalKind {
662 fn from(kind: ContentKind) -> Self {
663 match kind {
664 ContentKind::Claim => ProposalKind::Claims,
665 ContentKind::Plan => ProposalKind::Plan,
666 ContentKind::Classification => ProposalKind::Classification,
667 ContentKind::Evaluation => ProposalKind::Evaluation,
668 ContentKind::Draft => ProposalKind::DraftDocument,
669 ContentKind::Reasoning => ProposalKind::Reasoning,
670 }
671 }
672}
673
674#[derive(Debug, Clone, Serialize, Deserialize)]
679pub struct ProposedContent {
680 pub id: String,
682 pub kind: ContentKind,
684 pub content: String,
686 pub structured: Option<serde_json::Value>,
688 pub confidence: Option<f32>,
690 pub requires_human: bool,
692}
693
694impl ProposedContent {
695 #[must_use]
697 pub fn new(id: impl Into<String>, kind: ContentKind, content: impl Into<String>) -> Self {
698 Self {
699 id: id.into(),
700 kind,
701 content: content.into(),
702 structured: None,
703 confidence: None,
704 requires_human: false,
705 }
706 }
707
708 #[must_use]
710 pub fn with_human_required(mut self) -> Self {
711 self.requires_human = true;
712 self
713 }
714
715 #[must_use]
717 pub fn with_confidence(mut self, confidence: f32) -> Self {
718 self.confidence = Some(confidence);
719 self
720 }
721}
722
723#[derive(Debug, Clone, Serialize, Deserialize)]
725pub struct ContractResult {
726 pub name: String,
728 pub passed: bool,
730 pub failure_reason: Option<String>,
732}
733
734impl ContractResult {
735 #[must_use]
737 pub fn passed(name: impl Into<String>) -> Self {
738 Self {
739 name: name.into(),
740 passed: true,
741 failure_reason: None,
742 }
743 }
744
745 #[must_use]
747 pub fn failed(name: impl Into<String>, reason: impl Into<String>) -> Self {
748 Self {
749 name: name.into(),
750 passed: false,
751 failure_reason: Some(reason.into()),
752 }
753 }
754}
755
756#[derive(Debug, Clone, Serialize, Deserialize)]
767pub struct KernelProposal {
768 pub id: String,
770 pub kind: ProposalKind,
772 pub payload: String,
774 pub structured_payload: Option<serde_json::Value>,
776 pub trace_link: ReplayTrace,
778 pub contract_results: Vec<ContractResult>,
780 pub requires_human: bool,
782 pub confidence: Option<f32>,
784}
785
786impl KernelProposal {
787 #[must_use]
789 pub fn all_contracts_passed(&self) -> bool {
790 self.contract_results.iter().all(|r| r.passed)
791 }
792
793 #[must_use]
795 pub fn failed_contracts(&self) -> Vec<&str> {
796 self.contract_results
797 .iter()
798 .filter(|r| !r.passed)
799 .map(|r| r.name.as_str())
800 .collect()
801 }
802
803 #[must_use]
809 pub fn is_auto_promotable(&self) -> bool {
810 self.all_contracts_passed() && !self.requires_human
811 }
812}
813
814#[cfg(test)]
815mod tests {
816 use super::*;
817
818 #[test]
819 fn trace_link_replayability() {
820 let local = ReplayTrace::Local(LocalReplayTrace {
821 base_model_hash: "abc123".to_string(),
822 adapter: None,
823 tokenizer_hash: "tok123".to_string(),
824 seed: 42,
825 sampler: SamplerParams::default(),
826 prompt_version: "v1".to_string(),
827 recall: None,
828 weights_mutated: false,
829 execution_env: ExecutionEnv::default(),
830 });
831
832 let remote = ReplayTrace::Remote(RemoteReplayTrace {
833 provider_name: "anthropic".to_string(),
834 provider_model_id: "claude-3-opus".to_string(),
835 request_fingerprint: "req123".to_string(),
836 response_fingerprint: "resp456".to_string(),
837 temperature: 0.0,
838 top_p: 1.0,
839 max_tokens: 1024,
840 provider_metadata: HashMap::new(),
841 retried: false,
842 retry_reasons: vec![],
843 replayability: Replayability::BestEffort,
844 });
845
846 assert!(local.is_replay_eligible());
847 assert!(!remote.is_replay_eligible());
848
849 assert_eq!(local.replayability(), Replayability::Deterministic);
850 assert_eq!(remote.replayability(), Replayability::BestEffort);
851 }
852
853 #[test]
854 fn proposal_kind_conversion() {
855 assert_eq!(ProposalKind::from(ContentKind::Claim), ProposalKind::Claims);
856 assert_eq!(ProposalKind::from(ContentKind::Plan), ProposalKind::Plan);
857 assert_eq!(
858 ProposalKind::from(ContentKind::Reasoning),
859 ProposalKind::Reasoning
860 );
861 }
862
863 #[test]
864 fn contract_result_helpers() {
865 let passed = ContractResult::passed("grounded-answering");
866 assert!(passed.passed);
867 assert!(passed.failure_reason.is_none());
868
869 let failed = ContractResult::failed("reasoning", "missing CONCLUSION");
870 assert!(!failed.passed);
871 assert_eq!(failed.failure_reason.as_deref(), Some("missing CONCLUSION"));
872 }
873
874 #[test]
875 fn kernel_proposal_auto_promotable() {
876 let local_trace = ReplayTrace::Local(LocalReplayTrace {
877 base_model_hash: "hash".to_string(),
878 adapter: None,
879 tokenizer_hash: "tok".to_string(),
880 seed: 1,
881 sampler: SamplerParams::default(),
882 prompt_version: "v1".to_string(),
883 recall: None,
884 weights_mutated: false,
885 execution_env: ExecutionEnv::default(),
886 });
887
888 let promotable = KernelProposal {
890 id: "p1".to_string(),
891 kind: ProposalKind::Claims,
892 payload: "claim".to_string(),
893 structured_payload: None,
894 trace_link: local_trace.clone(),
895 contract_results: vec![ContractResult::passed("c1")],
896 requires_human: false,
897 confidence: Some(0.9),
898 };
899 assert!(promotable.is_auto_promotable());
900
901 let needs_human = KernelProposal {
903 id: "p2".to_string(),
904 kind: ProposalKind::Claims,
905 payload: "claim".to_string(),
906 structured_payload: None,
907 trace_link: local_trace.clone(),
908 contract_results: vec![ContractResult::passed("c1")],
909 requires_human: true,
910 confidence: Some(0.9),
911 };
912 assert!(!needs_human.is_auto_promotable());
913
914 let failed_contract = KernelProposal {
916 id: "p3".to_string(),
917 kind: ProposalKind::Claims,
918 payload: "claim".to_string(),
919 structured_payload: None,
920 trace_link: local_trace,
921 contract_results: vec![ContractResult::failed("c1", "reason")],
922 requires_human: false,
923 confidence: Some(0.9),
924 };
925 assert!(!failed_contract.is_auto_promotable());
926 }
927
928 #[test]
933 fn kernel_intent_builder() {
934 let intent = KernelIntent::new("analyze_metrics")
935 .with_criteria("find anomalies")
936 .with_criteria("suggest fixes")
937 .with_max_tokens(512);
938
939 assert_eq!(intent.task, "analyze_metrics");
940 assert_eq!(intent.criteria.len(), 2);
941 assert_eq!(intent.criteria[0], "find anomalies");
942 assert_eq!(intent.criteria[1], "suggest fixes");
943 assert_eq!(intent.max_tokens, 512);
944 }
945
946 #[test]
947 fn kernel_context_builder() {
948 let context = KernelContext::new()
949 .with_state("metric", serde_json::json!(0.5))
950 .with_fact("Seeds", "seed-1", "Some seed fact")
951 .with_tenant("tenant-123");
952
953 assert!(context.state.contains_key("metric"));
954 assert_eq!(context.facts.len(), 1);
955 assert_eq!(context.facts[0].key, "Seeds");
956 assert_eq!(context.facts[0].id, "seed-1");
957 assert_eq!(context.tenant_id, Some("tenant-123".to_string()));
958 }
959
960 #[test]
961 fn kernel_context_default() {
962 let context = KernelContext::default();
963 assert!(context.state.is_empty());
964 assert!(context.facts.is_empty());
965 assert!(context.tenant_id.is_none());
966 }
967
968 #[test]
969 fn kernel_policy_default() {
970 let policy = KernelPolicy::default();
971 assert!(policy.adapter_id.is_none());
972 assert!(!policy.recall_enabled);
973 assert_eq!(policy.recall_max_candidates, 5);
974 assert!((policy.recall_min_score - 0.7).abs() < f32::EPSILON);
975 assert!(policy.seed.is_none());
976 assert!(!policy.requires_human);
977 assert!(policy.required_truths.is_empty());
978 }
979
980 #[test]
981 fn kernel_policy_deterministic() {
982 let policy = KernelPolicy::deterministic(42)
983 .with_adapter("llm/grounded@1.0.0")
984 .with_recall(true)
985 .with_human_required()
986 .with_required_truth("grounded-answering");
987
988 assert_eq!(policy.seed, Some(42));
989 assert_eq!(policy.adapter_id, Some("llm/grounded@1.0.0".to_string()));
990 assert!(policy.recall_enabled);
991 assert!(policy.requires_human);
992 assert_eq!(policy.required_truths, vec!["grounded-answering"]);
993 }
994}