converge-core 3.2.1

Converge Agent OS - correctness-first, context-driven multi-agent runtime
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
// Copyright 2024-2026 Reflective Labs
// SPDX-License-Identifier: MIT

//! # Kernel Boundary Types
//!
//! These types define the **constitutional boundary** between reasoning kernels
//! (converge-llm) and the Converge platform. They encode core axioms:
//!
//! - **Proposed vs Fact**: Kernels emit `KernelProposal`, not `Fact`
//! - **Replayable vs Audit-only**: `LocalReplayTrace` vs `RemoteReplayTrace`
//! - **Explicit Authority**: All proposals have provenance via `ReplayTrace`
//!
//! ## Axiom Compliance
//!
//! | Axiom | Enforcement |
//! |-------|-------------|
//! | Agents Suggest, Engines Decide | `KernelProposal` cannot become `Fact` without validation |
//! | Transparent Determinism | `ReplayTrace` in every proposal |
//! | Human Authority First-Class | `requires_human` flag on proposals |
//!
//! ## Usage
//!
//! These types are re-exported by capability kernels (e.g., converge-llm)
//! but defined here to ensure a single source of truth across all kernels.

use serde::{Deserialize, Serialize};
use std::collections::HashMap;

// ============================================================================
// Kernel Input Types: The Platform-to-Kernel Contract
// ============================================================================

/// What the kernel should reason about.
///
/// This is the **intent contract** between the platform and any reasoning kernel.
/// It defines the task, success criteria, and resource budgets.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KernelIntent {
    /// The task to perform (e.g., "analyze_metrics", "generate_plan")
    pub task: String,
    /// Success criteria for the task
    pub criteria: Vec<String>,
    /// Maximum tokens budget for the entire kernel run
    pub max_tokens: usize,
}

impl KernelIntent {
    /// Create a new kernel intent with a task description.
    #[must_use]
    pub fn new(task: impl Into<String>) -> Self {
        Self {
            task: task.into(),
            criteria: Vec::new(),
            max_tokens: 1024,
        }
    }

    /// Add a success criterion.
    #[must_use]
    pub fn with_criteria(mut self, criteria: impl Into<String>) -> Self {
        self.criteria.push(criteria.into());
        self
    }

    /// Set maximum tokens budget.
    #[must_use]
    pub fn with_max_tokens(mut self, max_tokens: usize) -> Self {
        self.max_tokens = max_tokens;
        self
    }
}

/// The context provided to the kernel (from converge-core's Context).
///
/// This is a **read-only view** of the platform's context, projected
/// for kernel consumption. Kernels cannot mutate this directly.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KernelContext {
    /// Structured state data (from Seeds, Signals, etc.)
    pub state: HashMap<String, serde_json::Value>,
    /// Relevant facts from context (read-only view)
    pub facts: Vec<ContextFact>,
    /// Tenant/session identifier for recall scoping
    pub tenant_id: Option<String>,
}

impl KernelContext {
    /// Create an empty kernel context.
    #[must_use]
    pub fn new() -> Self {
        Self {
            state: HashMap::new(),
            facts: Vec::new(),
            tenant_id: None,
        }
    }

    /// Add state data.
    #[must_use]
    pub fn with_state(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
        self.state.insert(key.into(), value);
        self
    }

    /// Add a fact from the platform context.
    #[must_use]
    pub fn with_fact(
        mut self,
        key: impl Into<String>,
        id: impl Into<String>,
        content: impl Into<String>,
    ) -> Self {
        self.facts.push(ContextFact {
            key: key.into(),
            id: id.into(),
            content: content.into(),
        });
        self
    }

    /// Set tenant identifier for recall scoping.
    #[must_use]
    pub fn with_tenant(mut self, tenant_id: impl Into<String>) -> Self {
        self.tenant_id = Some(tenant_id.into());
        self
    }
}

impl Default for KernelContext {
    fn default() -> Self {
        Self::new()
    }
}

/// A fact from converge-core's context (read-only).
///
/// This is a projection of platform facts for kernel consumption.
/// The kernel cannot create or modify facts directly.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContextFact {
    /// The context key this fact belongs to
    pub key: String,
    /// Unique identifier for this fact
    pub id: String,
    /// The fact content
    pub content: String,
}

/// Policy controlling kernel behavior.
///
/// This is the **policy contract** from the platform/runtime to the kernel.
/// It controls adapter selection, recall behavior, determinism, and human gates.
///
/// # Axiom: Explicit Authority
///
/// Adapter selection comes from `KernelPolicy`, not emergent kernel behavior.
/// This ensures the platform maintains control over which capabilities are used.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KernelPolicy {
    /// Which adapter to use (explicit authority from outside)
    pub adapter_id: Option<String>,
    /// Whether recall is enabled for this run
    pub recall_enabled: bool,
    /// Maximum recall candidates to consider
    pub recall_max_candidates: usize,
    /// Minimum relevance score for recall results
    pub recall_min_score: f32,
    /// Seed for deterministic execution (None = random)
    pub seed: Option<u64>,
    /// Whether proposals from this run require human approval
    pub requires_human: bool,
    /// Truth targets that must pass for auto-promotion
    pub required_truths: Vec<String>,
}

impl KernelPolicy {
    /// Create a new default policy.
    #[must_use]
    pub fn new() -> Self {
        Self {
            adapter_id: None,
            recall_enabled: false,
            recall_max_candidates: 5,
            recall_min_score: 0.7,
            seed: None,
            requires_human: false,
            required_truths: Vec::new(),
        }
    }

    /// Create a deterministic policy with a fixed seed.
    #[must_use]
    pub fn deterministic(seed: u64) -> Self {
        Self {
            seed: Some(seed),
            ..Self::new()
        }
    }

    /// Set the adapter to use.
    #[must_use]
    pub fn with_adapter(mut self, adapter_id: impl Into<String>) -> Self {
        self.adapter_id = Some(adapter_id.into());
        self
    }

    /// Enable or disable recall.
    #[must_use]
    pub fn with_recall(mut self, enabled: bool) -> Self {
        self.recall_enabled = enabled;
        self
    }

    /// Mark proposals as requiring human approval.
    #[must_use]
    pub fn with_human_required(mut self) -> Self {
        self.requires_human = true;
        self
    }

    /// Add a required truth for auto-promotion.
    #[must_use]
    pub fn with_required_truth(mut self, truth: impl Into<String>) -> Self {
        self.required_truths.push(truth.into());
        self
    }
}

impl Default for KernelPolicy {
    fn default() -> Self {
        Self::new()
    }
}

// ============================================================================
// Routing Policy: Backend Selection Vocabulary
// ============================================================================

/// Risk tier for routing decisions.
///
/// This enum is part of the platform's vocabulary for backend selection.
/// Policies can restrict which backends are allowed for each risk tier.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum RiskTier {
    Low,
    Medium,
    High,
    Critical,
}

/// Data classification for routing decisions.
///
/// This enum controls which backends can handle data based on sensitivity.
/// Policies can restrict remote backends for confidential/restricted data.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum DataClassification {
    Public,
    Internal,
    Confidential,
    Restricted,
}

/// Policy for routing requests to backends.
///
/// Routing should be **policy-based**, not ad-hoc. This type encodes
/// the rules for selecting backends based on:
/// - Truth preferences (which truths prefer which backends)
/// - Risk tier (critical/high-risk operations may require local)
/// - Data classification (sensitive data may require local)
///
/// # Axiom: Explicit Authority
///
/// Backend selection is never implicit. Policies must explicitly allow
/// remote backends, and default-deny is the recommended stance.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RoutingPolicy {
    /// Truth target → preferred backend
    pub truth_preferences: HashMap<String, String>,
    /// Risk tier → allowed backends
    pub risk_tier_backends: HashMap<RiskTier, Vec<String>>,
    /// Data classification → allowed backends
    pub data_classification_backends: HashMap<DataClassification, Vec<String>>,
    /// Default backend if no rule matches
    pub default_backend: String,
}

impl Default for RoutingPolicy {
    fn default() -> Self {
        Self {
            truth_preferences: HashMap::new(),
            risk_tier_backends: HashMap::new(),
            data_classification_backends: HashMap::new(),
            default_backend: "local".to_string(),
        }
    }
}

impl RoutingPolicy {
    /// Create a policy that denies remote backends by default.
    ///
    /// Remote backends must be explicitly allowed via risk tier or data classification.
    /// This is the **recommended default** for security-conscious deployments.
    #[must_use]
    pub fn default_deny_remote() -> Self {
        let mut policy = Self::default();
        // Only allow local for high-risk and restricted data by default
        policy
            .risk_tier_backends
            .insert(RiskTier::Critical, vec!["local".to_string()]);
        policy
            .risk_tier_backends
            .insert(RiskTier::High, vec!["local".to_string()]);
        policy
            .data_classification_backends
            .insert(DataClassification::Restricted, vec!["local".to_string()]);
        policy
            .data_classification_backends
            .insert(DataClassification::Confidential, vec!["local".to_string()]);
        policy
    }

    /// Check if a backend is allowed for the given context.
    #[must_use]
    pub fn is_backend_allowed(
        &self,
        backend_name: &str,
        risk_tier: RiskTier,
        data_classification: DataClassification,
    ) -> bool {
        // Check if explicitly denied by risk tier
        if let Some(allowed) = self.risk_tier_backends.get(&risk_tier) {
            if !allowed.contains(&backend_name.to_string()) && !allowed.is_empty() {
                return false;
            }
        }

        // Check if explicitly denied by data classification
        if let Some(allowed) = self.data_classification_backends.get(&data_classification) {
            if !allowed.contains(&backend_name.to_string()) && !allowed.is_empty() {
                return false;
            }
        }

        true
    }

    /// Select a backend for the given request context.
    #[must_use]
    pub fn select_backend(
        &self,
        truth_ids: &[String],
        risk_tier: RiskTier,
        data_classification: DataClassification,
    ) -> &str {
        // Check truth preferences first
        for truth_id in truth_ids {
            if let Some(backend) = self.truth_preferences.get(truth_id) {
                return backend;
            }
        }

        // Check risk tier
        if let Some(backends) = self.risk_tier_backends.get(&risk_tier) {
            if let Some(backend) = backends.first() {
                return backend;
            }
        }

        // Check data classification
        if let Some(backends) = self.data_classification_backends.get(&data_classification) {
            if let Some(backend) = backends.first() {
                return backend;
            }
        }

        // Default
        &self.default_backend
    }
}

// ============================================================================
// Decision Step: Kernel Reasoning Phases
// ============================================================================

/// A step in the multi-phase reasoning process.
///
/// Kernels (like converge-llm) execute reasoning in distinct phases.
/// This enum represents those phases and is part of the kernel boundary
/// vocabulary for tracing, recall scoping, and contract validation.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum DecisionStep {
    /// First step: derive conclusions from state
    Reasoning,
    /// Second step: score/evaluate options
    Evaluation,
    /// Third step: produce action plan
    Planning,
}

impl DecisionStep {
    /// Get the expected contract type for this step.
    #[must_use]
    pub fn expected_contract(&self) -> &'static str {
        match self {
            Self::Reasoning => "Reasoning",
            Self::Evaluation => "Evaluation",
            Self::Planning => "Planning",
        }
    }

    /// Get the step name as a string.
    #[must_use]
    pub fn as_str(&self) -> &'static str {
        match self {
            Self::Reasoning => "reasoning",
            Self::Evaluation => "evaluation",
            Self::Planning => "planning",
        }
    }
}

impl Default for DecisionStep {
    fn default() -> Self {
        Self::Reasoning
    }
}

// ============================================================================
// ReplayTrace: Two Concrete Shapes
// ============================================================================

/// Trace link for audit and (possibly) replay.
///
/// The shape depends on the backend type. This is the **constitutional type**
/// that prevents "ReplayTrace + hope" semantics.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum ReplayTrace {
    /// Local backend: replay-eligible (deterministic)
    Local(LocalReplayTrace),
    /// Remote backend: audit-eligible only (bounded stochasticity)
    Remote(RemoteReplayTrace),
}

impl ReplayTrace {
    /// Check if this trace is replay-eligible (only local).
    #[must_use]
    pub fn is_replay_eligible(&self) -> bool {
        matches!(self, ReplayTrace::Local(_))
    }

    /// Get the replayability level.
    #[must_use]
    pub fn replayability(&self) -> Replayability {
        match self {
            ReplayTrace::Local(_) => Replayability::Deterministic,
            ReplayTrace::Remote(r) => r.replayability,
        }
    }
}

/// Replayability level of the trace.
///
/// This enum enforces explicit acknowledgment of replay guarantees.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum Replayability {
    /// Can be replayed with identical output (local inference)
    Deterministic,
    /// Best effort, may vary slightly (temp=0 remote)
    BestEffort,
    /// Cannot be replayed (external factors, safety layers)
    None,
}

impl Default for Replayability {
    fn default() -> Self {
        Self::None
    }
}

/// Reason why proposal replayability was downgraded.
///
/// This is a **stable contract surface** - serialization shape must not change
/// without careful migration planning. Used for audit trails showing which
/// component caused a replayability downgrade.
///
/// # Axiom: System Tells the Truth About Itself
///
/// If a kernel proposal includes stochastic components, the system must
/// explicitly document why replayability was downgraded, not silently degrade.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum ReplayabilityDowngradeReason {
    /// Recall embedder is not bit-exact deterministic
    RecallEmbedderNotDeterministic,
    /// Corpus content hash missing (cannot verify exact corpus state)
    RecallCorpusNotContentAddressed,
    /// Remote backend was used (cannot guarantee exact replay)
    RemoteBackendUsed,
    /// No seed was provided (inference is stochastic)
    NoSeedProvided,
    /// Multiple components caused downgrade
    MultipleReasons,
}

/// Local trace link — replay-eligible.
///
/// Contains all information needed to reproduce the exact output.
/// Only local inference can provide this level of determinism.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LocalReplayTrace {
    /// Hash of base model weights
    pub base_model_hash: String,
    /// Adapter ID + hash (if used)
    pub adapter: Option<AdapterTrace>,
    /// Tokenizer hash
    pub tokenizer_hash: String,
    /// Random seed used
    pub seed: u64,
    /// Sampler parameters
    pub sampler: SamplerParams,
    /// Prompt version
    pub prompt_version: String,
    /// Recall trace (if used)
    pub recall: Option<RecallTrace>,
    /// Whether weights were mutated (merge)
    pub weights_mutated: bool,
    /// Execution environment
    pub execution_env: ExecutionEnv,
}

/// Adapter trace for local runs.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AdapterTrace {
    pub adapter_id: String,
    pub adapter_hash: String,
    pub merged: bool,
}

/// Sampler parameters for reproducibility.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SamplerParams {
    pub temperature: f32,
    pub top_p: f32,
    pub top_k: Option<usize>,
}

impl Default for SamplerParams {
    fn default() -> Self {
        Self {
            temperature: 0.0,
            top_p: 1.0,
            top_k: None,
        }
    }
}

/// Recall trace for local runs.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RecallTrace {
    pub corpus_fingerprint: String,
    pub candidate_ids: Vec<String>,
    pub candidate_scores: Vec<f32>,
    pub injected_count: usize,
}

/// Execution environment info.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExecutionEnv {
    pub device: String,
    pub backend: String,
    pub precision: String,
}

impl Default for ExecutionEnv {
    fn default() -> Self {
        Self {
            device: "cpu".to_string(),
            backend: "ndarray".to_string(),
            precision: "f32".to_string(),
        }
    }
}

/// Remote trace link — audit-eligible only.
///
/// Contains enough info to audit but NOT replay deterministically.
/// This explicitly acknowledges the bounded stochasticity of remote providers.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RemoteReplayTrace {
    /// Provider name (e.g., "anthropic", "openai")
    pub provider_name: String,
    /// Model ID as returned by provider
    pub provider_model_id: String,
    /// Hash of canonicalized request
    pub request_fingerprint: String,
    /// Hash of response payload
    pub response_fingerprint: String,
    /// Temperature used
    pub temperature: f32,
    /// Top-p used
    pub top_p: f32,
    /// Max tokens requested
    pub max_tokens: usize,
    /// Provider-specific metadata (e.g., system_fingerprint)
    pub provider_metadata: HashMap<String, String>,
    /// Whether this was retried
    pub retried: bool,
    /// Retry reasons (if retried)
    pub retry_reasons: Vec<String>,
    /// Explicit replayability flag — prevents "ReplayTrace + hope" semantics
    pub replayability: Replayability,
}

// ============================================================================
// Proposal Types: The Kernel Output Boundary
// ============================================================================

/// The kind of proposal a kernel is making.
///
/// This taxonomy helps the engine understand what kind of validation
/// and promotion logic to apply.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum ProposalKind {
    /// Claims or assertions derived from reasoning
    Claims,
    /// An action plan with ordered steps
    Plan,
    /// A classification or categorization
    Classification,
    /// An evaluation with scores and justification
    Evaluation,
    /// A draft document or text artifact
    DraftDocument,
    /// Raw reasoning output (when no specific kind applies)
    Reasoning,
}

impl Default for ProposalKind {
    fn default() -> Self {
        Self::Reasoning
    }
}

/// Kind of proposed content (backend-level).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum ContentKind {
    Claim,
    Plan,
    Classification,
    Evaluation,
    Draft,
    Reasoning,
}

impl Default for ContentKind {
    fn default() -> Self {
        Self::Reasoning
    }
}

impl From<ContentKind> for ProposalKind {
    fn from(kind: ContentKind) -> Self {
        match kind {
            ContentKind::Claim => ProposalKind::Claims,
            ContentKind::Plan => ProposalKind::Plan,
            ContentKind::Classification => ProposalKind::Classification,
            ContentKind::Evaluation => ProposalKind::Evaluation,
            ContentKind::Draft => ProposalKind::DraftDocument,
            ContentKind::Reasoning => ProposalKind::Reasoning,
        }
    }
}

/// A proposed piece of content (not yet a Fact).
///
/// This is the backend-level proposal type. It gets wrapped in
/// `KernelProposal` at the kernel boundary.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProposedContent {
    /// Unique identifier
    pub id: String,
    /// The content type
    pub kind: ContentKind,
    /// The actual content
    pub content: String,
    /// Structured content (if applicable)
    pub structured: Option<serde_json::Value>,
    /// Confidence score (0.0 - 1.0)
    pub confidence: Option<f32>,
    /// Whether this requires human approval
    pub requires_human: bool,
}

impl ProposedContent {
    /// Create a new proposed content with minimal fields.
    #[must_use]
    pub fn new(id: impl Into<String>, kind: ContentKind, content: impl Into<String>) -> Self {
        Self {
            id: id.into(),
            kind,
            content: content.into(),
            structured: None,
            confidence: None,
            requires_human: false,
        }
    }

    /// Mark this proposal as requiring human approval.
    #[must_use]
    pub fn with_human_required(mut self) -> Self {
        self.requires_human = true;
        self
    }

    /// Add confidence score.
    #[must_use]
    pub fn with_confidence(mut self, confidence: f32) -> Self {
        self.confidence = Some(confidence);
        self
    }
}

/// Contract validation result for a proposal.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContractResult {
    /// Name of the contract or truth
    pub name: String,
    /// Whether it passed
    pub passed: bool,
    /// Failure reason if not passed
    pub failure_reason: Option<String>,
}

impl ContractResult {
    /// Create a passing result.
    #[must_use]
    pub fn passed(name: impl Into<String>) -> Self {
        Self {
            name: name.into(),
            passed: true,
            failure_reason: None,
        }
    }

    /// Create a failing result.
    #[must_use]
    pub fn failed(name: impl Into<String>, reason: impl Into<String>) -> Self {
        Self {
            name: name.into(),
            passed: false,
            failure_reason: Some(reason.into()),
        }
    }
}

/// A proposal from the reasoning kernel.
///
/// This is the **only** output type that crosses the kernel boundary.
/// It must be validated and promoted by converge-core before becoming a Fact.
///
/// # Axiom: "Agents Suggest, Engines Decide"
///
/// Kernels (including LLM kernels) emit `KernelProposal`, not `Fact`.
/// The engine validates proposals against contracts and truth requirements
/// before promoting them to facts.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KernelProposal {
    /// Unique identifier for this proposal
    pub id: String,
    /// What kind of proposal this is
    pub kind: ProposalKind,
    /// The actual content/payload
    pub payload: String,
    /// Structured payload (if applicable)
    pub structured_payload: Option<serde_json::Value>,
    /// Link to the generation trace (for audit/replay)
    pub trace_link: ReplayTrace,
    /// Contract/truth validation results
    pub contract_results: Vec<ContractResult>,
    /// Whether this proposal requires human approval
    pub requires_human: bool,
    /// Confidence score (0.0 - 1.0) if available
    pub confidence: Option<f32>,
}

impl KernelProposal {
    /// Check if all contracts passed.
    #[must_use]
    pub fn all_contracts_passed(&self) -> bool {
        self.contract_results.iter().all(|r| r.passed)
    }

    /// Get failed contract names.
    #[must_use]
    pub fn failed_contracts(&self) -> Vec<&str> {
        self.contract_results
            .iter()
            .filter(|r| !r.passed)
            .map(|r| r.name.as_str())
            .collect()
    }

    /// Check if this proposal is eligible for automatic promotion.
    ///
    /// A proposal can be auto-promoted if:
    /// - All contracts passed
    /// - Human approval is not required
    #[must_use]
    pub fn is_auto_promotable(&self) -> bool {
        self.all_contracts_passed() && !self.requires_human
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn trace_link_replayability() {
        let local = ReplayTrace::Local(LocalReplayTrace {
            base_model_hash: "abc123".to_string(),
            adapter: None,
            tokenizer_hash: "tok123".to_string(),
            seed: 42,
            sampler: SamplerParams::default(),
            prompt_version: "v1".to_string(),
            recall: None,
            weights_mutated: false,
            execution_env: ExecutionEnv::default(),
        });

        let remote = ReplayTrace::Remote(RemoteReplayTrace {
            provider_name: "anthropic".to_string(),
            provider_model_id: "claude-3-opus".to_string(),
            request_fingerprint: "req123".to_string(),
            response_fingerprint: "resp456".to_string(),
            temperature: 0.0,
            top_p: 1.0,
            max_tokens: 1024,
            provider_metadata: HashMap::new(),
            retried: false,
            retry_reasons: vec![],
            replayability: Replayability::BestEffort,
        });

        assert!(local.is_replay_eligible());
        assert!(!remote.is_replay_eligible());

        assert_eq!(local.replayability(), Replayability::Deterministic);
        assert_eq!(remote.replayability(), Replayability::BestEffort);
    }

    #[test]
    fn proposal_kind_conversion() {
        assert_eq!(ProposalKind::from(ContentKind::Claim), ProposalKind::Claims);
        assert_eq!(ProposalKind::from(ContentKind::Plan), ProposalKind::Plan);
        assert_eq!(
            ProposalKind::from(ContentKind::Reasoning),
            ProposalKind::Reasoning
        );
    }

    #[test]
    fn contract_result_helpers() {
        let passed = ContractResult::passed("grounded-answering");
        assert!(passed.passed);
        assert!(passed.failure_reason.is_none());

        let failed = ContractResult::failed("reasoning", "missing CONCLUSION");
        assert!(!failed.passed);
        assert_eq!(failed.failure_reason.as_deref(), Some("missing CONCLUSION"));
    }

    #[test]
    fn kernel_proposal_auto_promotable() {
        let local_trace = ReplayTrace::Local(LocalReplayTrace {
            base_model_hash: "hash".to_string(),
            adapter: None,
            tokenizer_hash: "tok".to_string(),
            seed: 1,
            sampler: SamplerParams::default(),
            prompt_version: "v1".to_string(),
            recall: None,
            weights_mutated: false,
            execution_env: ExecutionEnv::default(),
        });

        // All passed, no human required
        let promotable = KernelProposal {
            id: "p1".to_string(),
            kind: ProposalKind::Claims,
            payload: "claim".to_string(),
            structured_payload: None,
            trace_link: local_trace.clone(),
            contract_results: vec![ContractResult::passed("c1")],
            requires_human: false,
            confidence: Some(0.9),
        };
        assert!(promotable.is_auto_promotable());

        // Human required
        let needs_human = KernelProposal {
            id: "p2".to_string(),
            kind: ProposalKind::Claims,
            payload: "claim".to_string(),
            structured_payload: None,
            trace_link: local_trace.clone(),
            contract_results: vec![ContractResult::passed("c1")],
            requires_human: true,
            confidence: Some(0.9),
        };
        assert!(!needs_human.is_auto_promotable());

        // Contract failed
        let failed_contract = KernelProposal {
            id: "p3".to_string(),
            kind: ProposalKind::Claims,
            payload: "claim".to_string(),
            structured_payload: None,
            trace_link: local_trace,
            contract_results: vec![ContractResult::failed("c1", "reason")],
            requires_human: false,
            confidence: Some(0.9),
        };
        assert!(!failed_contract.is_auto_promotable());
    }

    // ========================================================================
    // Kernel Input Types Tests
    // ========================================================================

    #[test]
    fn kernel_intent_builder() {
        let intent = KernelIntent::new("analyze_metrics")
            .with_criteria("find anomalies")
            .with_criteria("suggest fixes")
            .with_max_tokens(512);

        assert_eq!(intent.task, "analyze_metrics");
        assert_eq!(intent.criteria.len(), 2);
        assert_eq!(intent.criteria[0], "find anomalies");
        assert_eq!(intent.criteria[1], "suggest fixes");
        assert_eq!(intent.max_tokens, 512);
    }

    #[test]
    fn kernel_context_builder() {
        let context = KernelContext::new()
            .with_state("metric", serde_json::json!(0.5))
            .with_fact("Seeds", "seed-1", "Some seed fact")
            .with_tenant("tenant-123");

        assert!(context.state.contains_key("metric"));
        assert_eq!(context.facts.len(), 1);
        assert_eq!(context.facts[0].key, "Seeds");
        assert_eq!(context.facts[0].id, "seed-1");
        assert_eq!(context.tenant_id, Some("tenant-123".to_string()));
    }

    #[test]
    fn kernel_context_default() {
        let context = KernelContext::default();
        assert!(context.state.is_empty());
        assert!(context.facts.is_empty());
        assert!(context.tenant_id.is_none());
    }

    #[test]
    fn kernel_policy_default() {
        let policy = KernelPolicy::default();
        assert!(policy.adapter_id.is_none());
        assert!(!policy.recall_enabled);
        assert_eq!(policy.recall_max_candidates, 5);
        assert!((policy.recall_min_score - 0.7).abs() < f32::EPSILON);
        assert!(policy.seed.is_none());
        assert!(!policy.requires_human);
        assert!(policy.required_truths.is_empty());
    }

    #[test]
    fn kernel_policy_deterministic() {
        let policy = KernelPolicy::deterministic(42)
            .with_adapter("llm/grounded@1.0.0")
            .with_recall(true)
            .with_human_required()
            .with_required_truth("grounded-answering");

        assert_eq!(policy.seed, Some(42));
        assert_eq!(policy.adapter_id, Some("llm/grounded@1.0.0".to_string()));
        assert!(policy.recall_enabled);
        assert!(policy.requires_human);
        assert_eq!(policy.required_truths, vec!["grounded-answering"]);
    }
}