Skip to main content

sgr_agent/
agent_runtime.rs

1//! AgentRuntime trait — unified access to runtime context for reasoning tools.
2//!
3//! Provides a single interface to all context systems: workflow phase,
4//! threat assessment, sender trust, skill hints, classification.
5//! Think tool builders use this instead of ad-hoc structs.
6
7/// Runtime context available to the agent during execution.
8/// Implement this to connect your pipeline, classifiers, and state machines.
9#[async_trait::async_trait]
10pub trait AgentRuntime: Send + Sync {
11    /// Current workflow phase (e.g. "Reading", "Acting", "Cleanup").
12    fn phase(&self) -> &str {
13        ""
14    }
15
16    /// Threat score for a specific file (0.0 = safe, 1.0 = attack).
17    fn threat_score(&self, _path: &str) -> f32 {
18        0.0
19    }
20
21    /// Sender trust level for an email address.
22    fn sender_trust(&self, _email: &str) -> SenderTrust {
23        SenderTrust::Unknown
24    }
25
26    /// Currently selected skill name (from classifier).
27    fn skill_name(&self) -> &str {
28        ""
29    }
30
31    /// Number of inbox files in current trial.
32    fn inbox_count(&self) -> usize {
33        0
34    }
35
36    /// Whether any inbox file contains OTP/credential content.
37    fn has_otp(&self) -> bool {
38        false
39    }
40
41    /// Whether any inbox file has injection/threat signals.
42    fn has_threat(&self) -> bool {
43        false
44    }
45
46    /// Classified intent (e.g. "intent_inbox", "intent_delete").
47    fn intent(&self) -> &str {
48        ""
49    }
50
51    /// Structured summary for think tool context injection.
52    fn context_summary(&self) -> String {
53        let mut parts = Vec::new();
54        let phase = self.phase();
55        if !phase.is_empty() {
56            parts.push(format!("phase={}", phase));
57        }
58        if self.inbox_count() > 0 {
59            parts.push(format!("inbox={}", self.inbox_count()));
60        }
61        if self.has_otp() {
62            parts.push("otp=true".to_string());
63        }
64        if self.has_threat() {
65            parts.push("threat=true".to_string());
66        }
67        let skill = self.skill_name();
68        if !skill.is_empty() {
69            parts.push(format!("skill={}", skill));
70        }
71        parts.join(" | ")
72    }
73}
74
75/// Sender trust levels (from CRM graph or domain matching).
76#[derive(Debug, Clone, PartialEq)]
77pub enum SenderTrust {
78    /// Known contact with matching domain.
79    Trusted,
80    /// Known contact but domain mismatch — possible impersonation.
81    Mismatch,
82    /// Not in CRM contacts.
83    Unknown,
84    /// Explicitly blocklisted.
85    Blocked,
86}
87
88/// Simple in-memory implementation of AgentRuntime.
89/// Populated from pipeline results, updated during execution.
90#[derive(Default)]
91pub struct SimpleRuntime {
92    pub phase: String,
93    pub intent: String,
94    pub inbox_count: usize,
95    pub has_otp: bool,
96    pub has_threat: bool,
97    pub skill_name: String,
98}
99
100#[async_trait::async_trait]
101impl AgentRuntime for SimpleRuntime {
102    fn phase(&self) -> &str {
103        &self.phase
104    }
105    fn skill_name(&self) -> &str {
106        &self.skill_name
107    }
108    fn inbox_count(&self) -> usize {
109        self.inbox_count
110    }
111    fn has_otp(&self) -> bool {
112        self.has_otp
113    }
114    fn has_threat(&self) -> bool {
115        self.has_threat
116    }
117    fn intent(&self) -> &str {
118        &self.intent
119    }
120}
121
122#[cfg(test)]
123mod tests {
124    use super::*;
125
126    #[test]
127    fn context_summary_empty() {
128        let rt = SimpleRuntime::default();
129        assert_eq!(rt.context_summary(), "");
130    }
131
132    #[test]
133    fn context_summary_full() {
134        let rt = SimpleRuntime {
135            phase: "Acting".into(),
136            inbox_count: 3,
137            has_otp: true,
138            has_threat: false,
139            skill_name: "inbox-processing".into(),
140            intent: "intent_inbox".into(),
141        };
142        let s = rt.context_summary();
143        assert!(s.contains("phase=Acting"));
144        assert!(s.contains("inbox=3"));
145        assert!(s.contains("otp=true"));
146        assert!(s.contains("skill=inbox-processing"));
147        assert!(!s.contains("threat"));
148    }
149
150    #[test]
151    fn context_summary_threat_only() {
152        let rt = SimpleRuntime {
153            has_threat: true,
154            ..Default::default()
155        };
156        let s = rt.context_summary();
157        assert!(s.contains("threat=true"));
158        assert!(!s.contains("otp"));
159        assert!(!s.contains("inbox"));
160    }
161
162    #[test]
163    fn context_summary_zero_inbox_hidden() {
164        let rt = SimpleRuntime {
165            inbox_count: 0,
166            ..Default::default()
167        };
168        assert!(!rt.context_summary().contains("inbox"));
169    }
170
171    #[test]
172    fn sender_trust_defaults_unknown() {
173        let rt = SimpleRuntime::default();
174        assert_eq!(rt.sender_trust("anyone@example.com"), SenderTrust::Unknown);
175    }
176
177    #[test]
178    fn default_runtime_all_safe() {
179        let rt = SimpleRuntime::default();
180        assert_eq!(rt.phase(), "");
181        assert_eq!(rt.inbox_count(), 0);
182        assert!(!rt.has_otp());
183        assert!(!rt.has_threat());
184        assert_eq!(rt.threat_score("any_file"), 0.0);
185    }
186}