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 pub verifier: Option<Arc<dyn vex_core::zk::ZkVerifier>>,
89}
90
91impl<L: LlmProvider + ?Sized> std::fmt::Debug for AgentExecutor<L> {
92 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
93 f.debug_struct("AgentExecutor")
94 .field("config", &self.config)
95 .field("identity", &self.identity)
96 .finish()
97 }
98}
99
100impl<L: LlmProvider + ?Sized> Clone for AgentExecutor<L> {
101 fn clone(&self) -> Self {
102 Self {
103 config: self.config.clone(),
104 llm: self.llm.clone(),
105 gate: self.gate.clone(),
106 audit_store: self.audit_store.clone(),
107 identity: self.identity.clone(),
108 verifier: self.verifier.clone(),
109 }
110 }
111}
112
113impl<L: LlmProvider + ?Sized> AgentExecutor<L> {
114 pub fn new(llm: Arc<L>, config: ExecutorConfig, gate: Arc<dyn Gate>) -> Self {
116 Self {
117 config,
118 llm,
119 gate,
120 audit_store: None,
121 identity: None,
122 verifier: None,
123 }
124 }
125
126 pub fn with_identity(
128 mut self,
129 identity: Arc<AgentIdentity>,
130 audit_store: Arc<AuditStore<dyn StorageBackend>>,
131 ) -> Self {
132 self.identity = Some(identity);
133 self.audit_store = Some(audit_store);
134 self
135 }
136
137 pub fn with_verifier(mut self, verifier: Arc<dyn vex_core::zk::ZkVerifier>) -> Self {
139 self.verifier = Some(verifier);
140 self
141 }
142
143 pub async fn execute(
145 &self,
146 tenant_id: &str, agent: &mut Agent,
148 prompt: &str,
149 intent_data: Option<vex_core::segment::IntentData>,
150 capabilities: Vec<Capability>,
151 ) -> Result<ExecutionResult, String> {
152 let full_prompt = if !agent.context.content.is_empty() {
154 format!(
155 "Previous Context (Time: {}):\n\"{}\"\n\nActive Prompt:\n\"{}\"",
156 agent.context.created_at, agent.context.content, prompt
157 )
158 } else {
159 prompt.to_string()
160 };
161
162 let blue_response = self
163 .llm
164 .complete(LlmRequest::with_role(&agent.config.role, &full_prompt))
165 .await
166 .map_err(|e| e.to_string())?
167 .content;
168
169 let (final_response, verified, confidence, debate) = if self.config.enable_adversarial {
171 self.run_adversarial_verification(agent, prompt, &blue_response)
172 .await?
173 } else {
174 (blue_response, false, 0.5, None)
175 };
176
177 let capsule = self
179 .gate
180 .execute_gate(
181 agent.id,
182 prompt,
183 &final_response,
184 intent_data,
185 confidence,
186 &capabilities,
187 Uuid::new_v4(),
188 )
189 .await;
190
191 if capsule.outcome == "HALT" {
192 return Err(format!("Gate Blocking (HALT): {}", capsule.reason_code));
193 }
194
195 if capsule.outcome == "ESCALATE" {
196 let escalation_id = capsule
197 .escalation_id
198 .clone()
199 .unwrap_or_else(|| "unknown".into());
200 tracing::warn!(agent_id = %agent.id, escalation_id = %escalation_id, "Gate requires EMS escalation before proceeding.");
201 return Err(format!("AEM_ESCALATION_REQUIRED: {}", escalation_id));
202 }
203
204 if !capabilities.is_empty() {
207 if let Some(token) = &capsule.continuation_token {
208 let aid = self.identity.as_ref().map(|id| id.agent_id.clone());
209
210 use sha2::{Digest, Sha256};
212 let intent_hash = hex::encode(Sha256::digest(final_response.as_bytes()));
213
214 self.gate
216 .verify_token(
217 token,
218 aid.as_deref(),
219 Some(&intent_hash),
220 Some(&token.payload.execution_target.circuit_id),
221 )
222 .await
223 .map_err(|e| format!("AEM_GOVERNANCE_VIOLATION: {}", e))?;
224
225 if let (Some(verifier), Some(intent)) = (&self.verifier, &capsule.intent_data) {
228 if let vex_core::segment::IntentData::Shadow { .. } = intent {
229 intent
230 .verify_shadow(verifier.as_ref())
231 .map_err(|e| format!("AEM_GOVERNANCE_VIOLATION (ZK_FAIL): {}", e))?;
232
233 tracing::info!(
234 agent_id = %agent.id,
235 "AEM: STARK proof re-verified locally for Shadow Intent."
236 );
237 }
238 }
239
240 tracing::info!(
241 agent_id = %agent.id,
242 intent_hash = %intent_hash,
243 "AEM: Governed execution permitted via validated token."
244 );
245 } else {
246 return Err("AEM_GOVERNANCE_VIOLATION: Privileged action requires a valid continuation token (Escalation Required).".to_string());
248 }
249 }
250
251 let mut context = ContextPacket::new(&final_response);
253 context.source_agent = Some(agent.id);
254 context.importance = confidence;
255
256 agent.context = context.clone();
258 agent.fitness = confidence;
259
260 let result = ExecutionResult {
261 agent_id: agent.id,
262 response: final_response,
263 verified,
264 confidence,
265 trace_root: context.trace_root.clone(),
266 context: context.clone(),
267 debate,
268 evidence: Some(capsule.clone()),
269 };
270
271 if let Some(store) = &self.audit_store {
273 let _ = store
274 .log(
275 tenant_id,
276 vex_core::audit::AuditEventType::AgentExecuted,
277 vex_core::audit::ActorType::Bot(agent.id),
278 Some(agent.id),
279 serde_json::json!({
280 "prompt": prompt,
281 "confidence": confidence,
282 "verified": verified,
283 }),
284 self.identity.as_ref().map(|id| id.as_ref()),
285 Some(capsule.witness_receipt.clone()),
286 capsule.vep_blob.clone(),
287 )
288 .await;
289 }
290
291 Ok(result)
292 }
293
294 async fn run_adversarial_verification(
296 &self,
297 blue_agent: &Agent,
298 _original_prompt: &str,
299 blue_response: &str,
300 ) -> Result<(String, bool, f64, Option<Debate>), String> {
301 let shadow = ShadowAgent::new(blue_agent, ShadowConfig::default());
303
304 let mut debate = Debate::new(blue_agent.id, shadow.agent.id, blue_response);
306
307 let mut consensus = Consensus::new(ConsensusProtocol::WeightedConfidence);
309
310 for round_num in 1..=self.config.max_debate_rounds {
312 let mut challenge_prompt = shadow.challenge_prompt(blue_response);
314 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.");
315
316 let red_output = self
317 .llm
318 .complete(LlmRequest::with_role(
319 &shadow.agent.config.role,
320 &challenge_prompt,
321 ))
322 .await
323 .map_err(|e| e.to_string())?
324 .content;
325
326 let (is_challenge, red_confidence, red_reasoning, _suggested_revision) =
328 if let Ok(res) = serde_json::from_str::<ChallengeResponse>(&red_output) {
329 (
330 res.is_challenge,
331 res.confidence,
332 res.reasoning,
333 res.suggested_revision,
334 )
335 } else if let Some(start) = red_output.find('{') {
336 if let Some(end) = red_output.rfind('}') {
337 if let Ok(res) =
338 serde_json::from_str::<ChallengeResponse>(&red_output[start..=end])
339 {
340 (
341 res.is_challenge,
342 res.confidence,
343 res.reasoning,
344 res.suggested_revision,
345 )
346 } else {
347 (true, 0.5, red_output.clone(), None)
349 }
350 } else {
351 (true, 0.5, "Parsing failed".to_string(), None)
353 }
354 } else {
355 (true, 0.5, "No JSON found".to_string(), None)
357 };
358
359 let rebuttal = if is_challenge {
360 let rebuttal_prompt = format!(
361 "Your previous response was challenged by a Red agent:\n\n\
362 Original: \"{}\"\n\n\
363 Challenge: \"{}\"\n\n\
364 Please address these concerns or provide a revised response.",
365 blue_response, red_reasoning
366 );
367 Some(
368 self.llm
369 .complete(LlmRequest::with_role(
370 &blue_agent.config.role,
371 &rebuttal_prompt,
372 ))
373 .await
374 .map_err(|e| e.to_string())?
375 .content,
376 )
377 } else {
378 None
379 };
380
381 debate.add_round(DebateRound {
382 round: round_num,
383 blue_claim: blue_response.to_string(),
384 red_challenge: red_reasoning.clone(),
385 blue_rebuttal: rebuttal,
386 });
387
388 consensus.add_vote(Vote {
390 agent_id: shadow.agent.id,
391 agrees: !is_challenge,
392 confidence: red_confidence,
393 reasoning: Some(red_reasoning),
394 });
395
396 if !is_challenge {
397 break;
398 }
399 }
400
401 let mut reflection_prompt = format!(
403 "You have just finished an adversarial debate about your original response.\n\n\
404 Original Response: \"{}\"\n\n\
405 Debate Rounds:\n",
406 blue_response
407 );
408
409 for (i, round) in debate.rounds.iter().enumerate() {
410 reflection_prompt.push_str(&format!(
411 "Round {}: Red challenged: \"{}\" -> You rebutted: \"{}\"\n",
412 i + 1,
413 round.red_challenge,
414 round.blue_rebuttal.as_deref().unwrap_or("N/A")
415 ));
416 }
417
418 reflection_prompt.push_str("\nBased on this debate, do you still stand by your original response? \
419 Respond in valid JSON: {\"agrees\": boolean, \"confidence\": float (0.0-1.0), \"reasoning\": \"string\"}.");
420
421 let blue_vote_res = self
422 .llm
423 .complete(LlmRequest::with_role(
424 &blue_agent.config.role,
425 &reflection_prompt,
426 ))
427 .await;
428
429 let (blue_agrees, blue_confidence, blue_reasoning) = if let Ok(resp) = blue_vote_res {
431 if let Ok(vote) = serde_json::from_str::<VoteResponse>(&resp.content) {
432 (vote.agrees, vote.confidence, vote.reflection)
433 } else if let Some(start) = resp.content.find('{') {
434 if let Some(end) = resp.content.rfind('}') {
435 if let Ok(vote) =
436 serde_json::from_str::<VoteResponse>(&resp.content[start..=end])
437 {
438 (vote.agrees, vote.confidence, vote.reflection)
439 } else {
440 (
441 false,
442 blue_agent.fitness,
443 "Failed to parse reflection JSON".to_string(),
444 )
445 }
446 } else {
447 (
448 false,
449 blue_agent.fitness,
450 "No JSON in reflection".to_string(),
451 )
452 }
453 } else {
454 (
455 false,
456 blue_agent.fitness,
457 "No reflection content".to_string(),
458 )
459 }
460 } else {
461 (
462 false,
463 blue_agent.fitness,
464 "Reflection LLM call failed".to_string(),
465 )
466 };
467
468 consensus.add_vote(Vote {
469 agent_id: blue_agent.id,
470 agrees: blue_agrees,
471 confidence: blue_confidence,
472 reasoning: Some(blue_reasoning),
473 });
474
475 consensus.evaluate();
476
477 let final_response = if consensus.reached && consensus.decision == Some(true) {
479 blue_response.to_string()
480 } else if let Some(last_round) = debate.rounds.last() {
481 last_round
483 .blue_rebuttal
484 .clone()
485 .unwrap_or_else(|| blue_response.to_string())
486 } else {
487 blue_response.to_string()
488 };
489
490 let verified = consensus.reached;
491 let confidence = consensus.confidence;
492
493 debate.conclude(consensus.decision.unwrap_or(false), confidence);
495
496 Ok((final_response, verified, confidence, Some(debate)))
497 }
498}
499
500#[cfg(test)]
501mod tests {
502 use super::*;
503 use vex_core::AgentConfig;
504
505 #[tokio::test]
506 async fn test_executor() {
507 use crate::gate::GenericGateMock;
508 use vex_llm::MockProvider;
509 let llm = Arc::new(MockProvider::smart());
510 let gate = Arc::new(GenericGateMock);
511 let config = ExecutorConfig {
512 enable_adversarial: false,
513 ..Default::default()
514 };
515 let executor = AgentExecutor::new(llm, config, gate);
516 let mut agent = Agent::new(AgentConfig::default());
517
518 let result = executor
519 .execute("test-tenant", &mut agent, "Test prompt", None, vec![])
520 .await
521 .unwrap();
522 assert!(!result.response.is_empty());
523 assert!(!result.verified);
525 }
526}