vex_runtime/
executor.rs

1//! Agent executor - runs individual agents with LLM backend
2
3use async_trait::async_trait;
4use std::sync::Arc;
5use uuid::Uuid;
6
7use vex_adversarial::{
8    Consensus, ConsensusProtocol, Debate, DebateRound, ShadowAgent, ShadowConfig, Vote,
9};
10use vex_core::{Agent, ContextPacket};
11
12/// Configuration for agent execution
13#[derive(Debug, Clone)]
14pub struct ExecutorConfig {
15    /// Maximum debate rounds
16    pub max_debate_rounds: u32,
17    /// Consensus protocol to use
18    pub consensus_protocol: ConsensusProtocol,
19    /// Whether to spawn shadow agents
20    pub enable_adversarial: bool,
21}
22
23impl Default for ExecutorConfig {
24    fn default() -> Self {
25        Self {
26            max_debate_rounds: 3,
27            consensus_protocol: ConsensusProtocol::Majority,
28            enable_adversarial: true,
29        }
30    }
31}
32
33/// Result of agent execution
34#[derive(Debug, Clone)]
35pub struct ExecutionResult {
36    /// The agent that produced this result
37    pub agent_id: Uuid,
38    /// The final response
39    pub response: String,
40    /// Whether it was verified by adversarial debate
41    pub verified: bool,
42    /// Confidence score (0.0 - 1.0)
43    pub confidence: f64,
44    /// Context packet with merkle hash
45    pub context: ContextPacket,
46    /// Debate details (if adversarial was enabled)
47    pub debate: Option<Debate>,
48}
49
50/// Trait for LLM provider (re-exported for convenience)
51#[async_trait]
52pub trait LlmBackend: Send + Sync {
53    async fn complete(&self, system: &str, prompt: &str) -> Result<String, String>;
54}
55
56/// Agent executor - runs agents with LLM backends
57pub struct AgentExecutor<L: LlmBackend> {
58    /// Configuration
59    pub config: ExecutorConfig,
60    /// LLM backend
61    llm: Arc<L>,
62}
63
64impl<L: LlmBackend> AgentExecutor<L> {
65    /// Create a new executor
66    pub fn new(llm: Arc<L>, config: ExecutorConfig) -> Self {
67        Self { config, llm }
68    }
69
70    /// Execute an agent with a prompt and return the result
71    pub async fn execute(
72        &self,
73        agent: &mut Agent,
74        prompt: &str,
75    ) -> Result<ExecutionResult, String> {
76        // Step 1: Get initial response from Blue agent
77        let blue_response = self.llm.complete(&agent.config.role, prompt).await?;
78
79        // Step 2: If adversarial is enabled, run debate
80        let (final_response, verified, confidence, debate) = if self.config.enable_adversarial {
81            self.run_adversarial_verification(agent, prompt, &blue_response)
82                .await?
83        } else {
84            (blue_response, false, 0.5, None)
85        };
86
87        // Step 3: Create context packet with hash
88        let mut context = ContextPacket::new(&final_response);
89        context.source_agent = Some(agent.id);
90        context.importance = confidence;
91
92        // Step 4: Update agent's context
93        agent.context = context.clone();
94        agent.fitness = confidence;
95
96        Ok(ExecutionResult {
97            agent_id: agent.id,
98            response: final_response,
99            verified,
100            confidence,
101            context,
102            debate,
103        })
104    }
105
106    /// Run adversarial verification with Red agent
107    async fn run_adversarial_verification(
108        &self,
109        blue_agent: &Agent,
110        _original_prompt: &str,
111        blue_response: &str,
112    ) -> Result<(String, bool, f64, Option<Debate>), String> {
113        // Create shadow agent
114        let shadow = ShadowAgent::new(blue_agent, ShadowConfig::default());
115
116        // Create debate
117        let mut debate = Debate::new(blue_agent.id, shadow.agent.id, blue_response);
118
119        // Run debate rounds
120        for round_num in 1..=self.config.max_debate_rounds {
121            // Red agent challenges
122            let challenge_prompt = shadow.challenge_prompt(blue_response);
123            let red_challenge = self
124                .llm
125                .complete(&shadow.agent.config.role, &challenge_prompt)
126                .await?;
127
128            // Blue agent rebuts (if there's something to rebut)
129            let rebuttal = if red_challenge.to_lowercase().contains("disagree")
130                || red_challenge.to_lowercase().contains("issue")
131                || red_challenge.to_lowercase().contains("concern")
132            {
133                let rebuttal_prompt = format!(
134                    "Your previous response was challenged:\n\n\
135                     Original: \"{}\"\n\n\
136                     Challenge: \"{}\"\n\n\
137                     Please address these concerns or revise your response.",
138                    blue_response, red_challenge
139                );
140                Some(
141                    self.llm
142                        .complete(&blue_agent.config.role, &rebuttal_prompt)
143                        .await?,
144                )
145            } else {
146                None
147            };
148
149            debate.add_round(DebateRound {
150                round: round_num,
151                blue_claim: blue_response.to_string(),
152                red_challenge,
153                blue_rebuttal: rebuttal,
154            });
155
156            // Check if we've reached consensus (Red agreed)
157            if debate
158                .rounds
159                .last()
160                .map(|r| r.red_challenge.to_lowercase().contains("agree"))
161                .unwrap_or(false)
162            {
163                break;
164            }
165        }
166
167        // Evaluate consensus
168        let mut consensus = Consensus::new(self.config.consensus_protocol);
169
170        // Blue's confidence depends on whether it successfully rebutted Red's challenges
171        // If Blue had to make a rebuttal, confidence is reduced
172        // If Red found issues Blue couldn't address, confidence is low
173        let blue_confidence = if let Some(last_round) = debate.rounds.last() {
174            let red_found_issues = last_round.red_challenge.to_lowercase().contains("issue")
175                || last_round.red_challenge.to_lowercase().contains("concern")
176                || last_round.red_challenge.to_lowercase().contains("disagree")
177                || last_round.red_challenge.to_lowercase().contains("flaw");
178
179            if red_found_issues {
180                // Red found issues - Blue's confidence depends on rebuttal quality
181                if last_round.blue_rebuttal.is_some() {
182                    0.6 // Reduced confidence - had to defend
183                } else {
184                    0.3 // Very low - couldn't defend
185                }
186            } else {
187                0.85 // Red agreed - high confidence
188            }
189        } else {
190            0.5 // No debate rounds - neutral
191        };
192
193        consensus.add_vote(Vote {
194            agent_id: blue_agent.id,
195            agrees: true,
196            confidence: blue_confidence,
197            reasoning: Some(format!("Blue confidence: {:.0}%", blue_confidence * 100.0)),
198        });
199
200        // Red votes based on final challenge
201        let red_agrees = debate
202            .rounds
203            .last()
204            .map(|r| !r.red_challenge.to_lowercase().contains("disagree"))
205            .unwrap_or(true);
206
207        consensus.add_vote(Vote {
208            agent_id: shadow.agent.id,
209            agrees: red_agrees,
210            confidence: 0.7,
211            reasoning: debate.rounds.last().map(|r| r.red_challenge.clone()),
212        });
213
214        consensus.evaluate();
215
216        // Determine final response
217        let final_response = if consensus.reached && consensus.decision == Some(true) {
218            blue_response.to_string()
219        } else if let Some(last_round) = debate.rounds.last() {
220            // Use rebuttal if available, otherwise original
221            last_round
222                .blue_rebuttal
223                .clone()
224                .unwrap_or_else(|| blue_response.to_string())
225        } else {
226            blue_response.to_string()
227        };
228
229        let verified = consensus.reached;
230        let confidence = consensus.confidence;
231
232        debate.conclude(consensus.decision.unwrap_or(true), confidence);
233
234        Ok((final_response, verified, confidence, Some(debate)))
235    }
236}
237
238#[cfg(test)]
239mod tests {
240    use super::*;
241    use vex_core::AgentConfig;
242
243    struct MockLlm;
244
245    #[async_trait]
246    impl LlmBackend for MockLlm {
247        async fn complete(&self, _system: &str, prompt: &str) -> Result<String, String> {
248            if prompt.contains("challenge") {
249                Ok("I agree with this assessment. The logic is sound.".to_string())
250            } else {
251                Ok("This is a test response.".to_string())
252            }
253        }
254    }
255
256    #[tokio::test]
257    async fn test_executor() {
258        let llm = Arc::new(MockLlm);
259        let executor = AgentExecutor::new(llm, ExecutorConfig::default());
260        let mut agent = Agent::new(AgentConfig::default());
261
262        let result = executor.execute(&mut agent, "Test prompt").await.unwrap();
263        assert!(!result.response.is_empty());
264        assert!(result.verified);
265    }
266}