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_hardware::api::AgentIdentity;
13use vex_llm::Capability;
14use vex_persist::{AuditStore, StorageBackend};
15
16#[derive(Debug, Deserialize)]
17struct ChallengeResponse {
18 is_challenge: bool,
19 confidence: f64,
20 reasoning: String,
21 suggested_revision: Option<String>,
22}
23
24#[derive(Debug, Deserialize)]
25struct VoteResponse {
26 agrees: bool,
27 reflection: String,
28 confidence: f64,
29}
30
31#[derive(Debug, Clone)]
33pub struct ExecutorConfig {
34 pub max_debate_rounds: u32,
36 pub consensus_protocol: ConsensusProtocol,
38 pub enable_adversarial: bool,
40}
41
42impl Default for ExecutorConfig {
43 fn default() -> Self {
44 Self {
45 max_debate_rounds: 3,
46 consensus_protocol: ConsensusProtocol::Majority,
47 enable_adversarial: true,
48 }
49 }
50}
51
52#[derive(Debug, Clone)]
54pub struct ExecutionResult {
55 pub agent_id: Uuid,
57 pub response: String,
59 pub verified: bool,
61 pub confidence: f64,
63 pub context: ContextPacket,
65 pub trace_root: Option<Hash>,
67 pub debate: Option<Debate>,
69 pub evidence: Option<vex_core::audit::EvidenceCapsule>,
71}
72
73use vex_llm::{LlmProvider, LlmRequest};
74
75pub struct AgentExecutor<L: LlmProvider + ?Sized> {
77 pub config: ExecutorConfig,
79 llm: Arc<L>,
81 gate: Arc<dyn Gate>,
83 pub audit_store: Option<Arc<AuditStore<dyn StorageBackend>>>,
85 pub identity: Option<Arc<AgentIdentity>>,
87}
88
89impl<L: LlmProvider + ?Sized> std::fmt::Debug for AgentExecutor<L> {
90 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
91 f.debug_struct("AgentExecutor")
92 .field("config", &self.config)
93 .field("identity", &self.identity)
94 .finish()
95 }
96}
97
98impl<L: LlmProvider + ?Sized> Clone for AgentExecutor<L> {
99 fn clone(&self) -> Self {
100 Self {
101 config: self.config.clone(),
102 llm: self.llm.clone(),
103 gate: self.gate.clone(),
104 audit_store: self.audit_store.clone(),
105 identity: self.identity.clone(),
106 }
107 }
108}
109
110impl<L: LlmProvider + ?Sized> AgentExecutor<L> {
111 pub fn new(llm: Arc<L>, config: ExecutorConfig, gate: Arc<dyn Gate>) -> Self {
113 Self {
114 config,
115 llm,
116 gate,
117 audit_store: None,
118 identity: None,
119 }
120 }
121
122 pub fn with_identity(
124 mut self,
125 identity: Arc<AgentIdentity>,
126 audit_store: Arc<AuditStore<dyn StorageBackend>>,
127 ) -> Self {
128 self.identity = Some(identity);
129 self.audit_store = Some(audit_store);
130 self
131 }
132
133 pub async fn execute(
135 &self,
136 tenant_id: &str, agent: &mut Agent,
138 prompt: &str,
139 capabilities: Vec<Capability>,
140 ) -> Result<ExecutionResult, String> {
141 let full_prompt = if !agent.context.content.is_empty() {
143 format!(
144 "Previous Context (Time: {}):\n\"{}\"\n\nActive Prompt:\n\"{}\"",
145 agent.context.created_at, agent.context.content, prompt
146 )
147 } else {
148 prompt.to_string()
149 };
150
151 let blue_response = self
152 .llm
153 .complete(LlmRequest::with_role(&agent.config.role, &full_prompt))
154 .await
155 .map_err(|e| e.to_string())?
156 .content;
157
158 let (final_response, verified, confidence, debate) = if self.config.enable_adversarial {
160 self.run_adversarial_verification(agent, prompt, &blue_response)
161 .await?
162 } else {
163 (blue_response, false, 0.5, None)
164 };
165
166 let capsule = self
168 .gate
169 .execute_gate(agent.id, prompt, &final_response, confidence, capabilities)
170 .await;
171
172 if capsule.outcome == "HALT" {
173 return Err(format!("Gate Blocking: {}", capsule.reason_code));
174 }
175
176 let mut context = ContextPacket::new(&final_response);
178 context.source_agent = Some(agent.id);
179 context.importance = confidence;
180
181 agent.context = context.clone();
183 agent.fitness = confidence;
184
185 let result = ExecutionResult {
186 agent_id: agent.id,
187 response: final_response,
188 verified,
189 confidence,
190 trace_root: context.trace_root.clone(),
191 context: context.clone(),
192 debate,
193 evidence: Some(capsule.clone()),
194 };
195
196 if let Some(store) = &self.audit_store {
198 let _ = store
199 .log(
200 tenant_id,
201 vex_core::audit::AuditEventType::AgentExecuted,
202 vex_core::audit::ActorType::Bot(agent.id),
203 Some(agent.id),
204 serde_json::json!({
205 "prompt": prompt,
206 "confidence": confidence,
207 "verified": verified,
208 }),
209 self.identity.as_ref().map(|id| id.as_ref()),
210 Some(capsule.witness_receipt.clone()),
211 capsule.vep_blob.clone(),
212 )
213 .await;
214 }
215
216 Ok(result)
217 }
218
219 async fn run_adversarial_verification(
221 &self,
222 blue_agent: &Agent,
223 _original_prompt: &str,
224 blue_response: &str,
225 ) -> Result<(String, bool, f64, Option<Debate>), String> {
226 let shadow = ShadowAgent::new(blue_agent, ShadowConfig::default());
228
229 let mut debate = Debate::new(blue_agent.id, shadow.agent.id, blue_response);
231
232 let mut consensus = Consensus::new(ConsensusProtocol::WeightedConfidence);
234
235 for round_num in 1..=self.config.max_debate_rounds {
237 let mut challenge_prompt = shadow.challenge_prompt(blue_response);
239 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.");
240
241 let red_output = self
242 .llm
243 .complete(LlmRequest::with_role(
244 &shadow.agent.config.role,
245 &challenge_prompt,
246 ))
247 .await
248 .map_err(|e| e.to_string())?
249 .content;
250
251 let (is_challenge, red_confidence, red_reasoning, _suggested_revision) =
253 if let Ok(res) = serde_json::from_str::<ChallengeResponse>(&red_output) {
254 (
255 res.is_challenge,
256 res.confidence,
257 res.reasoning,
258 res.suggested_revision,
259 )
260 } else if let Some(start) = red_output.find('{') {
261 if let Some(end) = red_output.rfind('}') {
262 if let Ok(res) =
263 serde_json::from_str::<ChallengeResponse>(&red_output[start..=end])
264 {
265 (
266 res.is_challenge,
267 res.confidence,
268 res.reasoning,
269 res.suggested_revision,
270 )
271 } else {
272 (true, 0.5, red_output.clone(), None)
274 }
275 } else {
276 (true, 0.5, "Parsing failed".to_string(), None)
278 }
279 } else {
280 (true, 0.5, "No JSON found".to_string(), None)
282 };
283
284 let rebuttal = if is_challenge {
285 let rebuttal_prompt = format!(
286 "Your previous response was challenged by a Red agent:\n\n\
287 Original: \"{}\"\n\n\
288 Challenge: \"{}\"\n\n\
289 Please address these concerns or provide a revised response.",
290 blue_response, red_reasoning
291 );
292 Some(
293 self.llm
294 .complete(LlmRequest::with_role(
295 &blue_agent.config.role,
296 &rebuttal_prompt,
297 ))
298 .await
299 .map_err(|e| e.to_string())?
300 .content,
301 )
302 } else {
303 None
304 };
305
306 debate.add_round(DebateRound {
307 round: round_num,
308 blue_claim: blue_response.to_string(),
309 red_challenge: red_reasoning.clone(),
310 blue_rebuttal: rebuttal,
311 });
312
313 consensus.add_vote(Vote {
315 agent_id: shadow.agent.id,
316 agrees: !is_challenge,
317 confidence: red_confidence,
318 reasoning: Some(red_reasoning),
319 });
320
321 if !is_challenge {
322 break;
323 }
324 }
325
326 let mut reflection_prompt = format!(
328 "You have just finished an adversarial debate about your original response.\n\n\
329 Original Response: \"{}\"\n\n\
330 Debate Rounds:\n",
331 blue_response
332 );
333
334 for (i, round) in debate.rounds.iter().enumerate() {
335 reflection_prompt.push_str(&format!(
336 "Round {}: Red challenged: \"{}\" -> You rebutted: \"{}\"\n",
337 i + 1,
338 round.red_challenge,
339 round.blue_rebuttal.as_deref().unwrap_or("N/A")
340 ));
341 }
342
343 reflection_prompt.push_str("\nBased on this debate, do you still stand by your original response? \
344 Respond in valid JSON: {\"agrees\": boolean, \"confidence\": float (0.0-1.0), \"reasoning\": \"string\"}.");
345
346 let blue_vote_res = self
347 .llm
348 .complete(LlmRequest::with_role(
349 &blue_agent.config.role,
350 &reflection_prompt,
351 ))
352 .await;
353
354 let (blue_agrees, blue_confidence, blue_reasoning) = if let Ok(resp) = blue_vote_res {
356 if let Ok(vote) = serde_json::from_str::<VoteResponse>(&resp.content) {
357 (vote.agrees, vote.confidence, vote.reflection)
358 } else if let Some(start) = resp.content.find('{') {
359 if let Some(end) = resp.content.rfind('}') {
360 if let Ok(vote) =
361 serde_json::from_str::<VoteResponse>(&resp.content[start..=end])
362 {
363 (vote.agrees, vote.confidence, vote.reflection)
364 } else {
365 (
366 false,
367 blue_agent.fitness,
368 "Failed to parse reflection JSON".to_string(),
369 )
370 }
371 } else {
372 (
373 false,
374 blue_agent.fitness,
375 "No JSON in reflection".to_string(),
376 )
377 }
378 } else {
379 (
380 false,
381 blue_agent.fitness,
382 "No reflection content".to_string(),
383 )
384 }
385 } else {
386 (
387 false,
388 blue_agent.fitness,
389 "Reflection LLM call failed".to_string(),
390 )
391 };
392
393 consensus.add_vote(Vote {
394 agent_id: blue_agent.id,
395 agrees: blue_agrees,
396 confidence: blue_confidence,
397 reasoning: Some(blue_reasoning),
398 });
399
400 consensus.evaluate();
401
402 let final_response = if consensus.reached && consensus.decision == Some(true) {
404 blue_response.to_string()
405 } else if let Some(last_round) = debate.rounds.last() {
406 last_round
408 .blue_rebuttal
409 .clone()
410 .unwrap_or_else(|| blue_response.to_string())
411 } else {
412 blue_response.to_string()
413 };
414
415 let verified = consensus.reached;
416 let confidence = consensus.confidence;
417
418 debate.conclude(consensus.decision.unwrap_or(false), confidence);
420
421 Ok((final_response, verified, confidence, Some(debate)))
422 }
423}
424
425#[cfg(test)]
426mod tests {
427 use super::*;
428 use vex_core::AgentConfig;
429
430 #[tokio::test]
431 async fn test_executor() {
432 use crate::gate::GenericGateMock;
433 use vex_llm::MockProvider;
434 let llm = Arc::new(MockProvider::smart());
435 let gate = Arc::new(GenericGateMock);
436 let config = ExecutorConfig {
437 enable_adversarial: false,
438 ..Default::default()
439 };
440 let executor = AgentExecutor::new(llm, config, gate);
441 let mut agent = Agent::new(AgentConfig::default());
442
443 let result = executor
444 .execute("test-tenant", &mut agent, "Test prompt", vec![])
445 .await
446 .unwrap();
447 assert!(!result.response.is_empty());
448 assert!(!result.verified);
450 }
451}