1use std::sync::Arc;
4use uuid::Uuid;
5
6use crate::gate::Gate;
7use serde::Deserialize;
8use vex_adversarial::{
9 Consensus, ConsensusProtocol, Debate, DebateRound, ShadowAgent, ShadowConfig, Vote,
10};
11use vex_core::{Agent, ContextPacket, Hash};
12use vex_llm::Capability;
13
14#[derive(Debug, Deserialize)]
15struct ChallengeResponse {
16 is_challenge: bool,
17 confidence: f64,
18 reasoning: String,
19 suggested_revision: Option<String>,
20}
21
22#[derive(Debug, Deserialize)]
23struct VoteResponse {
24 agrees: bool,
25 reflection: String,
26 confidence: f64,
27}
28
29#[derive(Debug, Clone)]
31pub struct ExecutorConfig {
32 pub max_debate_rounds: u32,
34 pub consensus_protocol: ConsensusProtocol,
36 pub enable_adversarial: bool,
38}
39
40impl Default for ExecutorConfig {
41 fn default() -> Self {
42 Self {
43 max_debate_rounds: 3,
44 consensus_protocol: ConsensusProtocol::Majority,
45 enable_adversarial: true,
46 }
47 }
48}
49
50#[derive(Debug, Clone)]
52pub struct ExecutionResult {
53 pub agent_id: Uuid,
55 pub response: String,
57 pub verified: bool,
59 pub confidence: f64,
61 pub context: ContextPacket,
63 pub trace_root: Option<Hash>,
65 pub debate: Option<Debate>,
67 pub evidence: Option<vex_core::audit::EvidenceCapsule>,
69}
70
71use vex_llm::{LlmProvider, LlmRequest};
72
73pub struct AgentExecutor<L: LlmProvider> {
75 pub config: ExecutorConfig,
77 llm: Arc<L>,
79 gate: Arc<dyn Gate>,
81}
82
83impl<L: LlmProvider> Clone for AgentExecutor<L> {
84 fn clone(&self) -> Self {
85 Self {
86 config: self.config.clone(),
87 llm: self.llm.clone(),
88 gate: self.gate.clone(),
89 }
90 }
91}
92
93impl<L: LlmProvider> AgentExecutor<L> {
94 pub fn new(llm: Arc<L>, config: ExecutorConfig, gate: Arc<dyn Gate>) -> Self {
96 Self { config, llm, gate }
97 }
98
99 pub async fn execute(
101 &self,
102 agent: &mut Agent,
103 prompt: &str,
104 capabilities: Vec<Capability>,
105 ) -> Result<ExecutionResult, String> {
106 let full_prompt = if !agent.context.content.is_empty() {
108 format!(
109 "Previous Context (Time: {}):\n\"{}\"\n\nActive Prompt:\n\"{}\"",
110 agent.context.created_at, agent.context.content, prompt
111 )
112 } else {
113 prompt.to_string()
114 };
115
116 let blue_response = self
117 .llm
118 .complete(LlmRequest::with_role(&agent.config.role, &full_prompt))
119 .await
120 .map_err(|e| e.to_string())?
121 .content;
122
123 let (final_response, verified, confidence, debate) = if self.config.enable_adversarial {
125 self.run_adversarial_verification(agent, prompt, &blue_response)
126 .await?
127 } else {
128 (blue_response, false, 0.5, None)
129 };
130
131 let capsule = self
133 .gate
134 .execute_gate(agent.id, prompt, &final_response, confidence, capabilities)
135 .await;
136
137 if capsule.outcome == "HALT" {
138 return Err(format!("Gate Blocking: {}", capsule.reason_code));
139 }
140
141 let mut context = ContextPacket::new(&final_response);
143 context.source_agent = Some(agent.id);
144 context.importance = confidence;
145
146 agent.context = context.clone();
148 agent.fitness = confidence;
149
150 Ok(ExecutionResult {
151 agent_id: agent.id,
152 response: final_response,
153 verified,
154 confidence,
155 trace_root: context.trace_root.clone(),
156 context,
157 debate,
158 evidence: Some(capsule),
159 })
160 }
161
162 async fn run_adversarial_verification(
164 &self,
165 blue_agent: &Agent,
166 _original_prompt: &str,
167 blue_response: &str,
168 ) -> Result<(String, bool, f64, Option<Debate>), String> {
169 let shadow = ShadowAgent::new(blue_agent, ShadowConfig::default());
171
172 let mut debate = Debate::new(blue_agent.id, shadow.agent.id, blue_response);
174
175 let mut consensus = Consensus::new(ConsensusProtocol::WeightedConfidence);
177
178 for round_num in 1..=self.config.max_debate_rounds {
180 let mut challenge_prompt = shadow.challenge_prompt(blue_response);
182 challenge_prompt.push_str("\n\nIMPORTANT: Respond in valid JSON format: {\"is_challenge\": boolean, \"confidence\": float (0.0-1.0), \"reasoning\": \"string\", \"suggested_revision\": \"string\" | null}. If you agree with the statement, set is_challenge to false.");
183
184 let red_output = self
185 .llm
186 .complete(LlmRequest::with_role(
187 &shadow.agent.config.role,
188 &challenge_prompt,
189 ))
190 .await
191 .map_err(|e| e.to_string())?
192 .content;
193
194 let (is_challenge, red_confidence, red_reasoning, _suggested_revision) =
196 if let Some(start) = red_output.find('{') {
197 if let Some(end) = red_output.rfind('}') {
198 if let Ok(res) =
199 serde_json::from_str::<ChallengeResponse>(&red_output[start..=end])
200 {
201 (
202 res.is_challenge,
203 res.confidence,
204 res.reasoning,
205 res.suggested_revision,
206 )
207 } else {
208 (
209 red_output.to_lowercase().contains("disagree"),
210 0.5,
211 red_output.clone(),
212 None,
213 )
214 }
215 } else {
216 (false, 0.0, "Parsing failed".to_string(), None)
217 }
218 } else {
219 (false, 0.0, "No JSON found".to_string(), None)
220 };
221
222 let rebuttal = if is_challenge {
223 let rebuttal_prompt = format!(
224 "Your previous response was challenged by a Red agent:\n\n\
225 Original: \"{}\"\n\n\
226 Challenge: \"{}\"\n\n\
227 Please address these concerns or provide a revised response.",
228 blue_response, red_reasoning
229 );
230 Some(
231 self.llm
232 .complete(LlmRequest::with_role(
233 &blue_agent.config.role,
234 &rebuttal_prompt,
235 ))
236 .await
237 .map_err(|e| e.to_string())?
238 .content,
239 )
240 } else {
241 None
242 };
243
244 debate.add_round(DebateRound {
245 round: round_num,
246 blue_claim: blue_response.to_string(),
247 red_challenge: red_reasoning.clone(),
248 blue_rebuttal: rebuttal,
249 });
250
251 consensus.add_vote(Vote {
253 agent_id: shadow.agent.id,
254 agrees: !is_challenge,
255 confidence: red_confidence,
256 reasoning: Some(red_reasoning),
257 });
258
259 if !is_challenge {
260 break;
261 }
262 }
263
264 let mut reflection_prompt = format!(
266 "You have just finished an adversarial debate about your original response.\n\n\
267 Original Response: \"{}\"\n\n\
268 Debate Rounds:\n",
269 blue_response
270 );
271
272 for (i, round) in debate.rounds.iter().enumerate() {
273 reflection_prompt.push_str(&format!(
274 "Round {}: Red challenged: \"{}\" -> You rebutted: \"{}\"\n",
275 i + 1,
276 round.red_challenge,
277 round.blue_rebuttal.as_deref().unwrap_or("N/A")
278 ));
279 }
280
281 reflection_prompt.push_str("\nBased on this debate, do you still stand by your original response? \
282 Respond in valid JSON: {\"agrees\": boolean, \"confidence\": float (0.0-1.0), \"reasoning\": \"string\"}.");
283
284 let blue_vote_res = self
285 .llm
286 .complete(LlmRequest::with_role(
287 &blue_agent.config.role,
288 &reflection_prompt,
289 ))
290 .await;
291
292 let (blue_agrees, blue_confidence, blue_reasoning) = if let Ok(resp) = blue_vote_res {
293 if let Some(start) = resp.content.find('{') {
294 if let Some(end) = resp.content.rfind('}') {
295 if let Ok(vote) =
296 serde_json::from_str::<VoteResponse>(&resp.content[start..=end])
297 {
298 (vote.agrees, vote.confidence, vote.reflection)
299 } else {
300 (
301 true,
302 blue_agent.fitness.max(0.5f64),
303 "Failed to parse reflection JSON".to_string(),
304 )
305 }
306 } else {
307 (
308 true,
309 blue_agent.fitness.max(0.5f64),
310 "No JSON in reflection".to_string(),
311 )
312 }
313 } else {
314 (
315 true,
316 blue_agent.fitness.max(0.5f64),
317 "No reflection content".to_string(),
318 )
319 }
320 } else {
321 (
322 true,
323 blue_agent.fitness.max(0.5f64),
324 "Reflection LLM call failed".to_string(),
325 )
326 };
327
328 consensus.add_vote(Vote {
329 agent_id: blue_agent.id,
330 agrees: blue_agrees,
331 confidence: blue_confidence.max(0.5f64),
332 reasoning: Some(blue_reasoning),
333 });
334
335 consensus.evaluate();
336
337 let final_response = if consensus.reached && consensus.decision == Some(true) {
339 blue_response.to_string()
340 } else if let Some(last_round) = debate.rounds.last() {
341 last_round
343 .blue_rebuttal
344 .clone()
345 .unwrap_or_else(|| blue_response.to_string())
346 } else {
347 blue_response.to_string()
348 };
349
350 let verified = consensus.reached;
351 let confidence = consensus.confidence;
352
353 debate.conclude(consensus.decision.unwrap_or(true), confidence);
354
355 Ok((final_response, verified, confidence, Some(debate)))
356 }
357}
358
359#[cfg(test)]
360mod tests {
361 use super::*;
362 use vex_core::AgentConfig;
363
364 #[tokio::test]
365 async fn test_executor() {
366 use crate::gate::GenericGateMock;
367 use vex_llm::MockProvider;
368 let llm = Arc::new(MockProvider::smart());
369 let gate = Arc::new(GenericGateMock);
370 let config = ExecutorConfig {
371 enable_adversarial: false,
372 ..Default::default()
373 };
374 let executor = AgentExecutor::new(llm, config, gate);
375 let mut agent = Agent::new(AgentConfig::default());
376
377 let result = executor
378 .execute(&mut agent, "Test prompt", vec![])
379 .await
380 .unwrap();
381 assert!(!result.response.is_empty());
382 assert!(!result.verified);
384 }
385}