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_issues(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    /// Detect potential areas of interest in a claim using pattern-based guidance
108    /// Returns a list of focus areas for the shadow agent to scrutinize.
109    pub fn detect_issues(&self, claim: &str) -> Vec<String> {
110        let mut areas_of_interest = Vec::new();
111        let claim_lower = claim.to_lowercase();
112
113        // Check for over-generalization
114        if claim_lower.contains("always")
115            || claim_lower.contains("never")
116            || claim_lower.contains("all ")
117            || claim_lower.contains("none ")
118        {
119            areas_of_interest.push("Over-generalization: Verify if universal claims 'always'/'never' hold true for all edge cases.".to_string());
120        }
121
122        // Check for quantificational ambiguity
123        if claim_lower.contains("many")
124            || claim_lower.contains("some")
125            || claim_lower.contains("significant")
126        {
127            areas_of_interest.push("Quantification: Assess if vague terms like 'many' or 'significant' hide a lack of specific data.".to_string());
128        }
129
130        // Check for evidence-free causality
131        if claim_lower.contains("because") || claim_lower.contains("therefore") {
132            areas_of_interest.push("Causality: Scrutinize the link between the premise and the conclusion for logical leaps.".to_string());
133        }
134
135        // Check for unsubstantiated numbers
136        if claim_lower.contains("%") || claim_lower.contains("percent") {
137            areas_of_interest.push("Statistics: Determine if percentages are sourced or if they are illustrative placeholders.".to_string());
138        }
139
140        // Check for certainty bias (loaded language)
141        let certainty_markers = ["obvious", "clearly", "undeniable", "proven", "fact"];
142        for word in certainty_markers {
143            if claim_lower.contains(word) {
144                areas_of_interest.push(format!("Certainty Bias: The use of '{}' may indicate a claim that assumes its own conclusion.", word));
145                break;
146            }
147        }
148
149        // Check for linguistic complexity
150        let avg_words_per_sentence =
151            claim.split_whitespace().count() / claim.matches('.').count().max(1);
152        if avg_words_per_sentence > 30 {
153            areas_of_interest.push("Complexity: The high sentence length may obscure specific errors or contradictions.".to_string());
154        }
155
156        areas_of_interest
157    }
158}
159
160#[cfg(test)]
161mod tests {
162    use super::*;
163
164    #[test]
165    fn test_shadow_creation() {
166        let blue = Agent::new(AgentConfig::default());
167        let shadow = ShadowAgent::new(&blue, ShadowConfig::default());
168
169        assert_eq!(shadow.blue_agent_id, blue.id);
170        assert!(!shadow.agent.config.spawn_shadow);
171    }
172
173    #[test]
174    fn test_detect_issues_universal_claims() {
175        let blue = Agent::new(AgentConfig::default());
176        let shadow = ShadowAgent::new(&blue, ShadowConfig::default());
177
178        let issues = shadow.detect_issues("This method always works without fail.");
179        assert!(issues.iter().any(|i| i.contains("Over-generalization")));
180    }
181
182    #[test]
183    fn test_detect_issues_statistics() {
184        let blue = Agent::new(AgentConfig::default());
185        let shadow = ShadowAgent::new(&blue, ShadowConfig::default());
186
187        let issues = shadow.detect_issues("Studies show 90% of users prefer this approach.");
188        assert!(issues.iter().any(|i| i.contains("Statistics")));
189    }
190
191    #[test]
192    fn test_detect_issues_loaded_language() {
193        let blue = Agent::new(AgentConfig::default());
194        let shadow = ShadowAgent::new(&blue, ShadowConfig::default());
195
196        let issues = shadow.detect_issues("It is obvious that the solution is correct.");
197        assert!(issues.iter().any(|i| i.contains("Certainty Bias")));
198    }
199
200    #[test]
201    fn test_detect_issues_clean_claim() {
202        let blue = Agent::new(AgentConfig::default());
203        let shadow = ShadowAgent::new(&blue, ShadowConfig::default());
204
205        // A clean, specific claim with no detected patterns
206        let issues = shadow.detect_issues("The API returns a 200 status code.");
207        // May still detect some issues, but should be fewer
208        assert!(issues.len() <= 2);
209    }
210}