Skip to main content

vex_adversarial/
shadow.rs

1//! Shadow agent spawning and management
2
3use serde::{Deserialize, Serialize};
4use uuid::Uuid;
5use vex_core::{Agent, AgentConfig};
6
7/// Configuration for shadow (adversarial) agents
8#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct ShadowConfig {
10    /// How aggressively the shadow should challenge
11    pub challenge_intensity: f64,
12    /// Whether to focus on factual accuracy
13    pub fact_check: bool,
14    /// Whether to check for logical consistency
15    pub logic_check: bool,
16}
17
18impl Default for ShadowConfig {
19    fn default() -> Self {
20        Self {
21            challenge_intensity: 0.7,
22            fact_check: true,
23            logic_check: true,
24        }
25    }
26}
27
28/// A shadow agent that challenges its paired Blue agent
29#[derive(Debug, Clone)]
30pub struct ShadowAgent {
31    /// The underlying agent
32    pub agent: Agent,
33    /// Shadow-specific configuration
34    pub config: ShadowConfig,
35    /// ID of the Blue agent this shadow is paired with
36    pub blue_agent_id: Uuid,
37}
38
39impl ShadowAgent {
40    /// Create a new shadow agent for the given Blue agent
41    pub fn new(blue_agent: &Agent, config: ShadowConfig) -> Self {
42        let agent_config = AgentConfig {
43            name: format!("{}_shadow", blue_agent.config.name),
44            role: format!(
45                "You are a critical challenger. Your job is to find flaws, \
46                 inconsistencies, and potential errors in the following claim. \
47                 Challenge intensity: {:.0}%",
48                config.challenge_intensity * 100.0
49            ),
50            max_depth: 0,        // Shadows don't spawn children
51            spawn_shadow: false, // Shadows don't have their own shadows
52        };
53
54        let mut agent = blue_agent.spawn_child(agent_config);
55        agent.shadow_id = None;
56
57        Self {
58            agent,
59            config,
60            blue_agent_id: blue_agent.id,
61        }
62    }
63
64    /// Generate a challenge prompt for the given claim with enhanced heuristics
65    pub fn challenge_prompt(&self, claim: &str) -> String {
66        let mut challenge_types = Vec::new();
67
68        if self.config.fact_check {
69            challenge_types.push("factual accuracy");
70        }
71        if self.config.logic_check {
72            challenge_types.push("logical consistency");
73        }
74
75        // Detect potential issues using pattern-based heuristics
76        let detected_issues = self.detect_areas_of_interest(claim);
77
78        // Build targeted challenge based on detected issues
79        let issue_guidance = if detected_issues.is_empty() {
80            String::from("Look for hidden assumptions, unstated premises, and edge cases.")
81        } else {
82            format!(
83                "Pay special attention to these potential issues: {}",
84                detected_issues.join("; ")
85            )
86        };
87
88        format!(
89            "Critically analyze the following claim for {}:\n\n\
90             \"{}\"\n\n\
91             {}\n\n\
92            For each issue found:
93            1. State the specific problem
94            2. Explain why it matters
95            3. Suggest how it could be verified or corrected
96
97            If any issues are found, start your response with the marker: [CHALLENGE]
98            If no issues are found, start your response with the marker: [CLEAN]
99
100            If [CLEAN], explain what makes the claim robust.",
101            challenge_types.join(" and "),
102            claim,
103            issue_guidance
104        )
105    }
106
107    /// Heuristic pre-filter using keyword matching, not comprehensive analysis.
108    /// Detects potential areas of interest for the shadow agent to scrutinize.
109    /// Returns a list of focus area descriptions — not confirmed issues.
110    pub fn detect_areas_of_interest(&self, claim: &str) -> Vec<String> {
111        let mut areas_of_interest = Vec::new();
112        let claim_lower = claim.to_lowercase();
113
114        // Check for over-generalization
115        if claim_lower.contains("always")
116            || claim_lower.contains("never")
117            || claim_lower.contains("all ")
118            || claim_lower.contains("none ")
119        {
120            areas_of_interest.push("Over-generalization: Verify if universal claims 'always'/'never' hold true for all edge cases.".to_string());
121        }
122
123        // Check for quantificational ambiguity
124        if claim_lower.contains("many")
125            || claim_lower.contains("some")
126            || claim_lower.contains("significant")
127        {
128            areas_of_interest.push("Quantification: Assess if vague terms like 'many' or 'significant' hide a lack of specific data.".to_string());
129        }
130
131        // Check for evidence-free causality
132        if claim_lower.contains("because") || claim_lower.contains("therefore") {
133            areas_of_interest.push("Causality: Scrutinize the link between the premise and the conclusion for logical leaps.".to_string());
134        }
135
136        // Check for unsubstantiated numbers
137        if claim_lower.contains("%") || claim_lower.contains("percent") {
138            areas_of_interest.push("Statistics: Determine if percentages are sourced or if they are illustrative placeholders.".to_string());
139        }
140
141        // Check for certainty bias (loaded language)
142        let certainty_markers = ["obvious", "clearly", "undeniable", "proven", "fact"];
143        for word in certainty_markers {
144            if claim_lower.contains(word) {
145                areas_of_interest.push(format!("Certainty Bias: The use of '{}' may indicate a claim that assumes its own conclusion.", word));
146                break;
147            }
148        }
149
150        // Check for linguistic complexity
151        let avg_words_per_sentence =
152            claim.split_whitespace().count() / claim.matches('.').count().max(1);
153        if avg_words_per_sentence > 30 {
154            areas_of_interest.push("Complexity: The high sentence length may obscure specific errors or contradictions.".to_string());
155        }
156
157        areas_of_interest
158    }
159
160    /// Backward-compatible alias for `detect_areas_of_interest`.
161    #[doc(hidden)]
162    pub fn detect_issues(&self, claim: &str) -> Vec<String> {
163        self.detect_areas_of_interest(claim)
164    }
165}
166
167#[cfg(test)]
168mod tests {
169    use super::*;
170
171    #[test]
172    fn test_shadow_creation() {
173        let blue = Agent::new(AgentConfig::default());
174        let shadow = ShadowAgent::new(&blue, ShadowConfig::default());
175
176        assert_eq!(shadow.blue_agent_id, blue.id);
177        assert!(!shadow.agent.config.spawn_shadow);
178    }
179
180    #[test]
181    fn test_detect_issues_universal_claims() {
182        let blue = Agent::new(AgentConfig::default());
183        let shadow = ShadowAgent::new(&blue, ShadowConfig::default());
184
185        let issues = shadow.detect_issues("This method always works without fail.");
186        assert!(issues.iter().any(|i| i.contains("Over-generalization")));
187    }
188
189    #[test]
190    fn test_detect_issues_statistics() {
191        let blue = Agent::new(AgentConfig::default());
192        let shadow = ShadowAgent::new(&blue, ShadowConfig::default());
193
194        let issues = shadow.detect_issues("Studies show 90% of users prefer this approach.");
195        assert!(issues.iter().any(|i| i.contains("Statistics")));
196    }
197
198    #[test]
199    fn test_detect_issues_loaded_language() {
200        let blue = Agent::new(AgentConfig::default());
201        let shadow = ShadowAgent::new(&blue, ShadowConfig::default());
202
203        let issues = shadow.detect_issues("It is obvious that the solution is correct.");
204        assert!(issues.iter().any(|i| i.contains("Certainty Bias")));
205    }
206
207    #[test]
208    fn test_detect_issues_clean_claim() {
209        let blue = Agent::new(AgentConfig::default());
210        let shadow = ShadowAgent::new(&blue, ShadowConfig::default());
211
212        // A clean, specific claim with no detected patterns
213        let issues = shadow.detect_issues("The API returns a 200 status code.");
214        // May still detect some issues, but should be fewer
215        assert!(issues.len() <= 2);
216    }
217}