Skip to main content

moloch_core/agent/
reasoning.rs

1//! Reasoning traces for agent decision transparency.
2//!
3//! Reasoning traces capture the agent's decision-making process. They answer:
4//! "Why did the agent decide to do this?"
5
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9use crate::crypto::{hash, Hash, PublicKey};
10use crate::error::{Error, Result};
11use crate::event::EventId;
12
13/// Unique trace identifier.
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
15pub struct TraceId(pub [u8; 16]);
16
17impl TraceId {
18    /// Generate a new random trace ID.
19    pub fn generate() -> Self {
20        use rand::RngCore;
21        let mut bytes = [0u8; 16];
22        rand::thread_rng().fill_bytes(&mut bytes);
23        Self(bytes)
24    }
25
26    /// Create from bytes.
27    pub fn from_bytes(bytes: [u8; 16]) -> Self {
28        Self(bytes)
29    }
30
31    /// Get the bytes.
32    pub fn as_bytes(&self) -> &[u8; 16] {
33        &self.0
34    }
35
36    /// Convert to hex string.
37    pub fn to_hex(&self) -> String {
38        hex::encode(self.0)
39    }
40
41    /// Parse from hex string.
42    pub fn from_hex(s: &str) -> Result<Self> {
43        let bytes = hex::decode(s).map_err(|_| Error::invalid_input("invalid hex"))?;
44        if bytes.len() != 16 {
45            return Err(Error::invalid_input("trace ID must be 16 bytes"));
46        }
47        let mut arr = [0u8; 16];
48        arr.copy_from_slice(&bytes);
49        Ok(Self(arr))
50    }
51}
52
53impl std::fmt::Display for TraceId {
54    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
55        write!(f, "{}", self.to_hex())
56    }
57}
58
59/// Priority level for goals.
60#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
61#[serde(rename_all = "snake_case")]
62pub enum Priority {
63    /// Background task, can be deferred.
64    Low,
65    /// Normal priority.
66    Normal,
67    /// Should be handled soon.
68    High,
69    /// Requires immediate attention.
70    Critical,
71}
72
73impl Default for Priority {
74    fn default() -> Self {
75        Self::Normal
76    }
77}
78
79impl std::fmt::Display for Priority {
80    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
81        match self {
82            Priority::Low => write!(f, "low"),
83            Priority::Normal => write!(f, "normal"),
84            Priority::High => write!(f, "high"),
85            Priority::Critical => write!(f, "critical"),
86        }
87    }
88}
89
90/// Source of a goal.
91#[derive(Debug, Clone, Serialize, Deserialize)]
92#[serde(tag = "type", rename_all = "snake_case")]
93pub enum GoalSource {
94    /// Direct user instruction.
95    UserInstruction { event_id: EventId },
96    /// Derived from higher-level goal.
97    Derived { parent_goal_description: String },
98    /// System-defined goal.
99    System { policy: String },
100}
101
102/// The goal driving agent behavior.
103#[derive(Debug, Clone, Serialize, Deserialize)]
104pub struct Goal {
105    /// Human-readable goal description.
106    description: String,
107    /// Structured goal representation.
108    structured: Option<serde_json::Value>,
109    /// Where this goal came from.
110    source: GoalSource,
111    /// Priority level.
112    priority: Priority,
113}
114
115impl Goal {
116    /// Create a new goal from user instruction.
117    pub fn from_user(description: impl Into<String>, event_id: EventId) -> Self {
118        Self {
119            description: description.into(),
120            structured: None,
121            source: GoalSource::UserInstruction { event_id },
122            priority: Priority::Normal,
123        }
124    }
125
126    /// Create a derived goal.
127    pub fn derived(description: impl Into<String>, parent_description: impl Into<String>) -> Self {
128        Self {
129            description: description.into(),
130            structured: None,
131            source: GoalSource::Derived {
132                parent_goal_description: parent_description.into(),
133            },
134            priority: Priority::Normal,
135        }
136    }
137
138    /// Create a system goal.
139    pub fn system(description: impl Into<String>, policy: impl Into<String>) -> Self {
140        Self {
141            description: description.into(),
142            structured: None,
143            source: GoalSource::System {
144                policy: policy.into(),
145            },
146            priority: Priority::Normal,
147        }
148    }
149
150    /// Set structured representation.
151    pub fn with_structured(mut self, structured: serde_json::Value) -> Self {
152        self.structured = Some(structured);
153        self
154    }
155
156    /// Set priority.
157    pub fn with_priority(mut self, priority: Priority) -> Self {
158        self.priority = priority;
159        self
160    }
161
162    /// Get the description.
163    pub fn description(&self) -> &str {
164        &self.description
165    }
166
167    /// Get the structured representation.
168    pub fn structured(&self) -> Option<&serde_json::Value> {
169        self.structured.as_ref()
170    }
171
172    /// Get the source.
173    pub fn source(&self) -> &GoalSource {
174        &self.source
175    }
176
177    /// Get the priority.
178    pub fn priority(&self) -> Priority {
179        self.priority
180    }
181}
182
183/// Action taken during a reasoning step.
184#[derive(Debug, Clone, Serialize, Deserialize)]
185#[serde(tag = "type", rename_all = "snake_case")]
186pub enum StepAction {
187    /// Retrieved information.
188    Retrieve { query: String, source: String },
189    /// Analyzed data.
190    Analyze { subject: String, method: String },
191    /// Invoked a tool.
192    ToolCall { tool: String, input_hash: Hash },
193    /// Delegated to another agent.
194    Delegate { agent: PublicKey, task: String },
195    /// Made a decision.
196    Decide { decision: String },
197}
198
199impl StepAction {
200    /// Create a retrieve action.
201    pub fn retrieve(query: impl Into<String>, source: impl Into<String>) -> Self {
202        Self::Retrieve {
203            query: query.into(),
204            source: source.into(),
205        }
206    }
207
208    /// Create an analyze action.
209    pub fn analyze(subject: impl Into<String>, method: impl Into<String>) -> Self {
210        Self::Analyze {
211            subject: subject.into(),
212            method: method.into(),
213        }
214    }
215
216    /// Create a tool call action.
217    pub fn tool_call(tool: impl Into<String>, input_hash: Hash) -> Self {
218        Self::ToolCall {
219            tool: tool.into(),
220            input_hash,
221        }
222    }
223
224    /// Create a delegate action.
225    pub fn delegate(agent: PublicKey, task: impl Into<String>) -> Self {
226        Self::Delegate {
227            agent,
228            task: task.into(),
229        }
230    }
231
232    /// Create a decide action.
233    pub fn decide(decision: impl Into<String>) -> Self {
234        Self::Decide {
235            decision: decision.into(),
236        }
237    }
238}
239
240/// A single step in reasoning.
241#[derive(Debug, Clone, Serialize, Deserialize)]
242pub struct ReasoningStep {
243    /// Step sequence number.
244    sequence: u32,
245    /// What the agent was thinking.
246    thought: String,
247    /// What action was taken (if any).
248    action: Option<StepAction>,
249    /// Observation from the action.
250    observation: Option<String>,
251    /// Timestamp of this step (Unix timestamp ms).
252    timestamp: i64,
253}
254
255impl ReasoningStep {
256    /// Create a new reasoning step.
257    pub fn new(sequence: u32, thought: impl Into<String>) -> Self {
258        Self {
259            sequence,
260            thought: thought.into(),
261            action: None,
262            observation: None,
263            timestamp: chrono::Utc::now().timestamp_millis(),
264        }
265    }
266
267    /// Set the action.
268    pub fn with_action(mut self, action: StepAction) -> Self {
269        self.action = Some(action);
270        self
271    }
272
273    /// Set the observation.
274    pub fn with_observation(mut self, observation: impl Into<String>) -> Self {
275        self.observation = Some(observation.into());
276        self
277    }
278
279    /// Set the timestamp.
280    pub fn with_timestamp(mut self, timestamp: i64) -> Self {
281        self.timestamp = timestamp;
282        self
283    }
284
285    /// Get the sequence number.
286    pub fn sequence(&self) -> u32 {
287        self.sequence
288    }
289
290    /// Get the thought.
291    pub fn thought(&self) -> &str {
292        &self.thought
293    }
294
295    /// Get the action.
296    pub fn action(&self) -> Option<&StepAction> {
297        self.action.as_ref()
298    }
299
300    /// Get the observation.
301    pub fn observation(&self) -> Option<&str> {
302        self.observation.as_deref()
303    }
304
305    /// Get the timestamp.
306    pub fn timestamp(&self) -> i64 {
307        self.timestamp
308    }
309}
310
311/// The decision reached.
312#[derive(Debug, Clone, Serialize, Deserialize)]
313pub struct Decision {
314    /// What was decided.
315    action: String,
316    /// Why this was chosen.
317    rationale: String,
318    /// Expected outcome.
319    expected_outcome: String,
320    /// How to verify success.
321    success_criteria: Vec<String>,
322}
323
324impl Decision {
325    /// Create a new decision.
326    pub fn new(
327        action: impl Into<String>,
328        rationale: impl Into<String>,
329        expected_outcome: impl Into<String>,
330    ) -> Self {
331        Self {
332            action: action.into(),
333            rationale: rationale.into(),
334            expected_outcome: expected_outcome.into(),
335            success_criteria: Vec::new(),
336        }
337    }
338
339    /// Add a success criterion.
340    pub fn with_criterion(mut self, criterion: impl Into<String>) -> Self {
341        self.success_criteria.push(criterion.into());
342        self
343    }
344
345    /// Add multiple success criteria.
346    pub fn with_criteria(mut self, criteria: Vec<String>) -> Self {
347        self.success_criteria = criteria;
348        self
349    }
350
351    /// Get the action.
352    pub fn action(&self) -> &str {
353        &self.action
354    }
355
356    /// Get the rationale.
357    pub fn rationale(&self) -> &str {
358        &self.rationale
359    }
360
361    /// Get the expected outcome.
362    pub fn expected_outcome(&self) -> &str {
363        &self.expected_outcome
364    }
365
366    /// Get the success criteria.
367    pub fn success_criteria(&self) -> &[String] {
368        &self.success_criteria
369    }
370}
371
372/// Confidence assessment.
373#[derive(Debug, Clone, Serialize, Deserialize)]
374pub struct Confidence {
375    /// Overall confidence score (0.0 - 1.0).
376    score: f64,
377    /// Confidence breakdown by factor.
378    breakdown: HashMap<String, f64>,
379    /// Uncertainty sources.
380    uncertainties: Vec<String>,
381    /// What would increase confidence.
382    would_help: Vec<String>,
383}
384
385impl Confidence {
386    /// Create a new confidence assessment.
387    pub fn new(score: f64) -> Self {
388        Self {
389            score: score.clamp(0.0, 1.0),
390            breakdown: HashMap::new(),
391            uncertainties: Vec::new(),
392            would_help: Vec::new(),
393        }
394    }
395
396    /// Create high confidence (0.9).
397    pub fn high() -> Self {
398        Self::new(0.9)
399    }
400
401    /// Create medium confidence (0.7).
402    pub fn medium() -> Self {
403        Self::new(0.7)
404    }
405
406    /// Create low confidence (0.4).
407    pub fn low() -> Self {
408        Self::new(0.4)
409    }
410
411    /// Add a breakdown factor.
412    pub fn with_factor(mut self, factor: impl Into<String>, score: f64) -> Self {
413        self.breakdown.insert(factor.into(), score.clamp(0.0, 1.0));
414        self
415    }
416
417    /// Add an uncertainty.
418    pub fn with_uncertainty(mut self, uncertainty: impl Into<String>) -> Self {
419        self.uncertainties.push(uncertainty.into());
420        self
421    }
422
423    /// Add something that would help.
424    pub fn with_would_help(mut self, help: impl Into<String>) -> Self {
425        self.would_help.push(help.into());
426        self
427    }
428
429    /// Get the score.
430    pub fn score(&self) -> f64 {
431        self.score
432    }
433
434    /// Get the breakdown.
435    pub fn breakdown(&self) -> &HashMap<String, f64> {
436        &self.breakdown
437    }
438
439    /// Get the uncertainties.
440    pub fn uncertainties(&self) -> &[String] {
441        &self.uncertainties
442    }
443
444    /// Get what would help.
445    pub fn would_help(&self) -> &[String] {
446        &self.would_help
447    }
448
449    /// Check if confidence is below the rejection threshold (0.3).
450    pub fn should_reject(&self) -> bool {
451        self.score < 0.3
452    }
453
454    /// Check if confidence is below the approval threshold (0.5).
455    pub fn requires_approval(&self) -> bool {
456        self.score < 0.5
457    }
458
459    /// Check if confidence is below the warning threshold (0.7).
460    pub fn should_warn(&self) -> bool {
461        self.score < 0.7
462    }
463}
464
465/// An alternative that was considered.
466#[derive(Debug, Clone, Serialize, Deserialize)]
467pub struct Alternative {
468    /// Description of the alternative.
469    description: String,
470    /// Why it was not chosen.
471    rejection_reason: String,
472    /// Estimated outcome if chosen.
473    estimated_outcome: String,
474    /// Confidence if this were chosen.
475    confidence: f64,
476}
477
478impl Alternative {
479    /// Create a new alternative.
480    pub fn new(
481        description: impl Into<String>,
482        rejection_reason: impl Into<String>,
483        estimated_outcome: impl Into<String>,
484        confidence: f64,
485    ) -> Self {
486        Self {
487            description: description.into(),
488            rejection_reason: rejection_reason.into(),
489            estimated_outcome: estimated_outcome.into(),
490            confidence: confidence.clamp(0.0, 1.0),
491        }
492    }
493
494    /// Get the description.
495    pub fn description(&self) -> &str {
496        &self.description
497    }
498
499    /// Get the rejection reason.
500    pub fn rejection_reason(&self) -> &str {
501        &self.rejection_reason
502    }
503
504    /// Get the estimated outcome.
505    pub fn estimated_outcome(&self) -> &str {
506        &self.estimated_outcome
507    }
508
509    /// Get the confidence.
510    pub fn confidence(&self) -> f64 {
511        self.confidence
512    }
513}
514
515/// A factor influencing the decision.
516#[derive(Debug, Clone, Serialize, Deserialize)]
517pub struct Factor {
518    /// Factor description.
519    description: String,
520    /// How much it influenced (positive = toward, negative = against).
521    influence: f64,
522    /// Evidence supporting this factor.
523    evidence: Vec<String>,
524}
525
526impl Factor {
527    /// Create a new factor.
528    pub fn new(description: impl Into<String>, influence: f64) -> Self {
529        Self {
530            description: description.into(),
531            influence: influence.clamp(-1.0, 1.0),
532            evidence: Vec::new(),
533        }
534    }
535
536    /// Create a positive factor (toward the decision).
537    pub fn positive(description: impl Into<String>, influence: f64) -> Self {
538        Self::new(description, influence.abs())
539    }
540
541    /// Create a negative factor (against the decision).
542    pub fn negative(description: impl Into<String>, influence: f64) -> Self {
543        Self::new(description, -influence.abs())
544    }
545
546    /// Add evidence.
547    pub fn with_evidence(mut self, evidence: impl Into<String>) -> Self {
548        self.evidence.push(evidence.into());
549        self
550    }
551
552    /// Get the description.
553    pub fn description(&self) -> &str {
554        &self.description
555    }
556
557    /// Get the influence.
558    pub fn influence(&self) -> f64 {
559        self.influence
560    }
561
562    /// Get the evidence.
563    pub fn evidence(&self) -> &[String] {
564        &self.evidence
565    }
566
567    /// Check if this factor supports the decision.
568    pub fn is_supportive(&self) -> bool {
569        self.influence > 0.0
570    }
571}
572
573/// Complete trace of agent reasoning.
574#[derive(Debug, Clone, Serialize, Deserialize)]
575pub struct ReasoningTrace {
576    /// Unique trace identifier.
577    id: TraceId,
578    /// The goal the agent was pursuing.
579    goal: Goal,
580    /// Steps in the reasoning process.
581    steps: Vec<ReasoningStep>,
582    /// Final decision reached.
583    decision: Decision,
584    /// Confidence in the decision.
585    confidence: Confidence,
586    /// Alternative actions considered.
587    alternatives: Vec<Alternative>,
588    /// Factors that influenced the decision.
589    factors: Vec<Factor>,
590    /// Hash of the full trace for integrity.
591    trace_hash: Hash,
592}
593
594impl ReasoningTrace {
595    /// Create a new reasoning trace builder.
596    pub fn builder() -> ReasoningTraceBuilder {
597        ReasoningTraceBuilder::new()
598    }
599
600    /// Get the trace ID.
601    pub fn id(&self) -> TraceId {
602        self.id
603    }
604
605    /// Get the goal.
606    pub fn goal(&self) -> &Goal {
607        &self.goal
608    }
609
610    /// Get the steps.
611    pub fn steps(&self) -> &[ReasoningStep] {
612        &self.steps
613    }
614
615    /// Get the decision.
616    pub fn decision(&self) -> &Decision {
617        &self.decision
618    }
619
620    /// Get the confidence.
621    pub fn confidence(&self) -> &Confidence {
622        &self.confidence
623    }
624
625    /// Get the alternatives.
626    pub fn alternatives(&self) -> &[Alternative] {
627        &self.alternatives
628    }
629
630    /// Get the factors.
631    pub fn factors(&self) -> &[Factor] {
632        &self.factors
633    }
634
635    /// Get the trace hash.
636    pub fn trace_hash(&self) -> Hash {
637        self.trace_hash
638    }
639
640    /// Compute the canonical bytes for hashing.
641    pub fn canonical_bytes(&self) -> Vec<u8> {
642        let mut data = Vec::new();
643        data.extend_from_slice(&self.id.0);
644
645        // Include goal
646        let goal_json = serde_json::to_vec(&self.goal).unwrap_or_default();
647        data.extend_from_slice(&goal_json);
648
649        // Include steps
650        for step in &self.steps {
651            let step_json = serde_json::to_vec(step).unwrap_or_default();
652            data.extend_from_slice(&step_json);
653        }
654
655        // Include decision
656        let decision_json = serde_json::to_vec(&self.decision).unwrap_or_default();
657        data.extend_from_slice(&decision_json);
658
659        // Include confidence
660        data.extend_from_slice(&self.confidence.score.to_le_bytes());
661
662        // Include alternatives
663        for alt in &self.alternatives {
664            let alt_json = serde_json::to_vec(alt).unwrap_or_default();
665            data.extend_from_slice(&alt_json);
666        }
667
668        // Include factors
669        for factor in &self.factors {
670            let factor_json = serde_json::to_vec(factor).unwrap_or_default();
671            data.extend_from_slice(&factor_json);
672        }
673
674        data
675    }
676
677    /// Verify the trace hash matches the content.
678    pub fn verify_integrity(&self) -> bool {
679        let computed = hash(&self.canonical_bytes());
680        computed == self.trace_hash
681    }
682
683    /// Check if this trace is complete per spec rule 7.3.2.
684    pub fn is_complete(&self) -> bool {
685        // Must have goal
686        if self.goal.description.is_empty() {
687            return false;
688        }
689
690        // Must have at least one step
691        if self.steps.is_empty() {
692            return false;
693        }
694
695        // Must have decision
696        if self.decision.action.is_empty() {
697            return false;
698        }
699
700        // Confidence score must be valid
701        if !(0.0..=1.0).contains(&self.confidence.score) {
702            return false;
703        }
704
705        true
706    }
707}
708
709/// Builder for ReasoningTrace.
710#[derive(Debug, Default)]
711pub struct ReasoningTraceBuilder {
712    id: Option<TraceId>,
713    goal: Option<Goal>,
714    steps: Vec<ReasoningStep>,
715    decision: Option<Decision>,
716    confidence: Option<Confidence>,
717    alternatives: Vec<Alternative>,
718    factors: Vec<Factor>,
719}
720
721impl ReasoningTraceBuilder {
722    /// Create a new builder.
723    pub fn new() -> Self {
724        Self::default()
725    }
726
727    /// Set the trace ID.
728    pub fn id(mut self, id: TraceId) -> Self {
729        self.id = Some(id);
730        self
731    }
732
733    /// Set the goal.
734    pub fn goal(mut self, goal: Goal) -> Self {
735        self.goal = Some(goal);
736        self
737    }
738
739    /// Add a reasoning step.
740    pub fn step(mut self, step: ReasoningStep) -> Self {
741        self.steps.push(step);
742        self
743    }
744
745    /// Add multiple steps.
746    pub fn steps(mut self, steps: Vec<ReasoningStep>) -> Self {
747        self.steps = steps;
748        self
749    }
750
751    /// Set the decision.
752    pub fn decision(mut self, decision: Decision) -> Self {
753        self.decision = Some(decision);
754        self
755    }
756
757    /// Set the confidence.
758    pub fn confidence(mut self, confidence: Confidence) -> Self {
759        self.confidence = Some(confidence);
760        self
761    }
762
763    /// Add an alternative.
764    pub fn alternative(mut self, alternative: Alternative) -> Self {
765        self.alternatives.push(alternative);
766        self
767    }
768
769    /// Add a factor.
770    pub fn factor(mut self, factor: Factor) -> Self {
771        self.factors.push(factor);
772        self
773    }
774
775    /// Build the reasoning trace.
776    pub fn build(self) -> Result<ReasoningTrace> {
777        let id = self.id.unwrap_or_else(TraceId::generate);
778
779        let goal = self
780            .goal
781            .ok_or_else(|| Error::invalid_input("goal is required"))?;
782
783        if self.steps.is_empty() {
784            return Err(Error::invalid_input(
785                "at least one reasoning step is required",
786            ));
787        }
788
789        let decision = self
790            .decision
791            .ok_or_else(|| Error::invalid_input("decision is required"))?;
792
793        let confidence = self.confidence.unwrap_or_else(Confidence::medium);
794
795        let mut trace = ReasoningTrace {
796            id,
797            goal,
798            steps: self.steps,
799            decision,
800            confidence,
801            alternatives: self.alternatives,
802            factors: self.factors,
803            trace_hash: Hash::from_bytes([0u8; 32]), // Placeholder
804        };
805
806        // Compute the hash
807        trace.trace_hash = hash(&trace.canonical_bytes());
808
809        Ok(trace)
810    }
811}
812
813#[cfg(test)]
814mod tests {
815    use super::*;
816
817    fn test_event_id() -> EventId {
818        EventId(hash(b"test-event"))
819    }
820
821    // === TraceId Tests ===
822
823    #[test]
824    fn trace_id_generates_unique() {
825        let id1 = TraceId::generate();
826        let id2 = TraceId::generate();
827        assert_ne!(id1, id2);
828    }
829
830    #[test]
831    fn trace_id_hex_roundtrip() {
832        let id = TraceId::generate();
833        let hex = id.to_hex();
834        let restored = TraceId::from_hex(&hex).unwrap();
835        assert_eq!(id, restored);
836    }
837
838    // === Priority Tests ===
839
840    #[test]
841    fn priority_ordering() {
842        assert!(Priority::Low < Priority::Normal);
843        assert!(Priority::Normal < Priority::High);
844        assert!(Priority::High < Priority::Critical);
845    }
846
847    // === Goal Tests ===
848
849    #[test]
850    fn goal_from_user() {
851        let goal = Goal::from_user("Complete the task", test_event_id());
852        assert_eq!(goal.description(), "Complete the task");
853        assert!(matches!(goal.source(), GoalSource::UserInstruction { .. }));
854    }
855
856    #[test]
857    fn goal_derived() {
858        let goal = Goal::derived("Sub-task", "Parent task");
859        assert_eq!(goal.description(), "Sub-task");
860        assert!(matches!(goal.source(), GoalSource::Derived { .. }));
861    }
862
863    #[test]
864    fn goal_with_priority() {
865        let goal =
866            Goal::from_user("Urgent task", test_event_id()).with_priority(Priority::Critical);
867        assert_eq!(goal.priority(), Priority::Critical);
868    }
869
870    // === ReasoningStep Tests ===
871
872    #[test]
873    fn reasoning_step_basic() {
874        let step = ReasoningStep::new(1, "Analyzing the problem");
875        assert_eq!(step.sequence(), 1);
876        assert_eq!(step.thought(), "Analyzing the problem");
877        assert!(step.action().is_none());
878    }
879
880    #[test]
881    fn reasoning_step_with_action() {
882        let step = ReasoningStep::new(1, "Looking up information")
883            .with_action(StepAction::retrieve("user data", "database"));
884        assert!(step.action().is_some());
885    }
886
887    #[test]
888    fn reasoning_step_with_observation() {
889        let step = ReasoningStep::new(1, "Checking status").with_observation("Status is active");
890        assert_eq!(step.observation(), Some("Status is active"));
891    }
892
893    // === Decision Tests ===
894
895    #[test]
896    fn decision_basic() {
897        let decision = Decision::new(
898            "Proceed with option A",
899            "It has the highest success probability",
900            "Task completed successfully",
901        );
902        assert_eq!(decision.action(), "Proceed with option A");
903        assert_eq!(
904            decision.rationale(),
905            "It has the highest success probability"
906        );
907    }
908
909    #[test]
910    fn decision_with_criteria() {
911        let decision = Decision::new("Execute", "Best option", "Success")
912            .with_criterion("Output matches expected format")
913            .with_criterion("No errors in logs");
914        assert_eq!(decision.success_criteria().len(), 2);
915    }
916
917    // === Confidence Tests ===
918
919    #[test]
920    fn confidence_clamped() {
921        let conf = Confidence::new(1.5);
922        assert_eq!(conf.score(), 1.0);
923
924        let conf2 = Confidence::new(-0.5);
925        assert_eq!(conf2.score(), 0.0);
926    }
927
928    #[test]
929    fn confidence_thresholds() {
930        let low = Confidence::new(0.2);
931        assert!(low.should_reject());
932        assert!(low.requires_approval());
933        assert!(low.should_warn());
934
935        let medium = Confidence::new(0.6);
936        assert!(!medium.should_reject());
937        assert!(!medium.requires_approval());
938        assert!(medium.should_warn());
939
940        let high = Confidence::new(0.8);
941        assert!(!high.should_reject());
942        assert!(!high.requires_approval());
943        assert!(!high.should_warn());
944    }
945
946    #[test]
947    fn confidence_with_breakdown() {
948        let conf = Confidence::medium()
949            .with_factor("data_quality", 0.8)
950            .with_factor("model_accuracy", 0.6)
951            .with_uncertainty("Limited training data");
952
953        assert_eq!(conf.breakdown().len(), 2);
954        assert_eq!(conf.uncertainties().len(), 1);
955    }
956
957    // === Alternative Tests ===
958
959    #[test]
960    fn alternative_basic() {
961        let alt = Alternative::new(
962            "Option B",
963            "Higher cost",
964            "Same result but more expensive",
965            0.6,
966        );
967        assert_eq!(alt.description(), "Option B");
968        assert_eq!(alt.rejection_reason(), "Higher cost");
969    }
970
971    // === Factor Tests ===
972
973    #[test]
974    fn factor_positive() {
975        let factor = Factor::positive("Strong evidence", 0.8);
976        assert!(factor.is_supportive());
977        assert_eq!(factor.influence(), 0.8);
978    }
979
980    #[test]
981    fn factor_negative() {
982        let factor = Factor::negative("Risk of failure", 0.3);
983        assert!(!factor.is_supportive());
984        assert_eq!(factor.influence(), -0.3);
985    }
986
987    #[test]
988    fn factor_with_evidence() {
989        let factor = Factor::positive("Proven approach", 0.9)
990            .with_evidence("Study A shows 95% success rate")
991            .with_evidence("Historical data confirms");
992        assert_eq!(factor.evidence().len(), 2);
993    }
994
995    // === ReasoningTrace Tests ===
996
997    #[test]
998    fn reasoning_trace_requires_goal() {
999        let result = ReasoningTrace::builder()
1000            .step(ReasoningStep::new(1, "Thinking"))
1001            .decision(Decision::new("Do it", "Because", "Success"))
1002            .build();
1003        assert!(result.is_err());
1004    }
1005
1006    #[test]
1007    fn reasoning_trace_requires_steps() {
1008        let result = ReasoningTrace::builder()
1009            .goal(Goal::from_user("Task", test_event_id()))
1010            .decision(Decision::new("Do it", "Because", "Success"))
1011            .build();
1012        assert!(result.is_err());
1013    }
1014
1015    #[test]
1016    fn reasoning_trace_requires_decision() {
1017        let result = ReasoningTrace::builder()
1018            .goal(Goal::from_user("Task", test_event_id()))
1019            .step(ReasoningStep::new(1, "Thinking"))
1020            .build();
1021        assert!(result.is_err());
1022    }
1023
1024    #[test]
1025    fn reasoning_trace_complete() {
1026        let trace = ReasoningTrace::builder()
1027            .goal(Goal::from_user("Complete the task", test_event_id()))
1028            .step(ReasoningStep::new(1, "Analyzing requirements"))
1029            .step(ReasoningStep::new(2, "Evaluating options"))
1030            .decision(Decision::new(
1031                "Use approach A",
1032                "Most efficient",
1033                "Task completed",
1034            ))
1035            .confidence(Confidence::high())
1036            .alternative(Alternative::new("Approach B", "Slower", "Same result", 0.7))
1037            .factor(Factor::positive("Clear requirements", 0.9))
1038            .build()
1039            .unwrap();
1040
1041        assert!(trace.is_complete());
1042        assert_eq!(trace.steps().len(), 2);
1043        assert_eq!(trace.alternatives().len(), 1);
1044        assert_eq!(trace.factors().len(), 1);
1045    }
1046
1047    #[test]
1048    fn reasoning_trace_hash_computed() {
1049        let trace = ReasoningTrace::builder()
1050            .goal(Goal::from_user("Task", test_event_id()))
1051            .step(ReasoningStep::new(1, "Thinking"))
1052            .decision(Decision::new("Do it", "Because", "Success"))
1053            .build()
1054            .unwrap();
1055
1056        // Hash should not be all zeros
1057        assert_ne!(trace.trace_hash().as_bytes(), &[0u8; 32]);
1058    }
1059
1060    #[test]
1061    fn reasoning_trace_integrity_verification() {
1062        let trace = ReasoningTrace::builder()
1063            .goal(Goal::from_user("Task", test_event_id()))
1064            .step(ReasoningStep::new(1, "Thinking"))
1065            .decision(Decision::new("Do it", "Because", "Success"))
1066            .build()
1067            .unwrap();
1068
1069        assert!(trace.verify_integrity());
1070    }
1071
1072    #[test]
1073    fn reasoning_trace_tamper_detected() {
1074        let mut trace = ReasoningTrace::builder()
1075            .goal(Goal::from_user("Task", test_event_id()))
1076            .step(ReasoningStep::new(1, "Thinking"))
1077            .decision(Decision::new("Do it", "Because", "Success"))
1078            .build()
1079            .unwrap();
1080
1081        // Tamper with the decision
1082        trace.decision = Decision::new("Do something else", "Changed", "Different");
1083
1084        // Integrity check should fail
1085        assert!(!trace.verify_integrity());
1086    }
1087
1088    // === StepAction Tests ===
1089
1090    #[test]
1091    fn step_action_variants() {
1092        let retrieve = StepAction::retrieve("query", "db");
1093        assert!(matches!(retrieve, StepAction::Retrieve { .. }));
1094
1095        let analyze = StepAction::analyze("data", "statistical");
1096        assert!(matches!(analyze, StepAction::Analyze { .. }));
1097
1098        let tool = StepAction::tool_call("bash", hash(b"input"));
1099        assert!(matches!(tool, StepAction::ToolCall { .. }));
1100
1101        let decide = StepAction::decide("Go ahead");
1102        assert!(matches!(decide, StepAction::Decide { .. }));
1103    }
1104}