1use crate::audit::{AuditEventType, AuditLog};
7use crate::config::Config;
8use crate::error::Result;
9use crate::llm::{
10 ChatMessage, Choice, LLMProviderTrait, MultiModelManager, ProviderFallbackChain, RetryConfig,
11 TokenBudget,
12};
13use crate::mcp::McpClient;
14use crate::policy::{Decision, PolicyEngine};
15use crate::ravenfabric::RavenFabricClient;
16use crate::sandbox::Sandbox;
17use crate::tools::{ToolCall, ToolRegistry, ToolResult};
18use serde::{Deserialize, Serialize};
19use std::path::PathBuf;
20use std::sync::Arc;
21use tokio::sync::RwLock;
22use tracing::{debug, info, instrument, warn};
23
24#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct ConversationMemory {
30 max_messages: usize,
32 messages: Vec<ChatMessage>,
34}
35
36impl ConversationMemory {
37 pub fn new(system_prompt: &str, max_messages: usize) -> Self {
40 Self {
41 max_messages,
42 messages: vec![ChatMessage::new("system", system_prompt.to_string())],
43 }
44 }
45
46 pub fn add_user_message(&mut self, content: &str) -> &[ChatMessage] {
48 self.messages
49 .push(ChatMessage::new("user", content.to_string()));
50 self.trim_to_max();
51 &self.messages
52 }
53
54 pub fn add_user_message_with_images(
56 &mut self,
57 text: &str,
58 image_data_uris: Vec<String>,
59 ) -> &[ChatMessage] {
60 self.messages.push(ChatMessage::with_images(
61 "user",
62 text.to_string(),
63 image_data_uris,
64 ));
65 self.trim_to_max();
66 &self.messages
67 }
68
69 pub fn add_assistant_message(&mut self, content: &str) {
71 self.messages
72 .push(ChatMessage::new("assistant", content.to_string()));
73 self.trim_to_max();
74 }
75
76 pub fn history(&self) -> &[ChatMessage] {
78 &self.messages
79 }
80
81 pub fn from_history(messages: Vec<ChatMessage>, max_messages: usize) -> Self {
84 Self {
85 max_messages,
86 messages,
87 }
88 }
89
90 #[allow(dead_code)]
92 pub fn len(&self) -> usize {
93 self.messages.len()
94 }
95
96 #[allow(dead_code)]
98 pub fn is_empty(&self) -> bool {
99 self.messages.is_empty()
100 }
101
102 fn trim_to_max(&mut self) {
104 if self.max_messages == 0 {
105 return;
106 }
107 while self.messages.len() > self.max_messages {
108 if self.messages.len() > 1 {
110 self.messages.remove(1);
111 } else {
112 break;
113 }
114 }
115 }
116}
117
118#[derive(Debug, Clone, Serialize, Deserialize)]
124pub struct CheckpointState {
125 pub session_id: String,
127 pub iteration: usize,
129 pub max_iterations: usize,
131 pub messages: Vec<ChatMessage>,
133 pub initial_prompt: String,
135 pub system_prompt: String,
137 pub provider: String,
139 pub model: String,
141 pub enable_tools: bool,
143 pub last_checkpoint: String,
145}
146
147impl CheckpointState {
148 #[allow(clippy::too_many_arguments)]
150 pub fn new(
151 session_id: String,
152 iteration: usize,
153 max_iterations: usize,
154 messages: Vec<ChatMessage>,
155 initial_prompt: &str,
156 system_prompt: &str,
157 provider: &str,
158 model: &str,
159 enable_tools: bool,
160 ) -> Self {
161 Self {
162 session_id,
163 iteration,
164 max_iterations,
165 messages,
166 initial_prompt: initial_prompt.to_string(),
167 system_prompt: system_prompt.to_string(),
168 provider: provider.to_string(),
169 model: model.to_string(),
170 enable_tools,
171 last_checkpoint: chrono::Utc::now().to_rfc3339(),
172 }
173 }
174}
175
176pub fn save_checkpoint(
181 checkpoint_dir: &std::path::Path,
182 state: &CheckpointState,
183) -> std::result::Result<std::path::PathBuf, String> {
184 let path = checkpoint_dir.join(format!("{}.json", state.session_id));
185
186 std::fs::create_dir_all(checkpoint_dir)
188 .map_err(|e| format!("Failed to create checkpoint directory: {}", e))?;
189
190 let content = serde_json::to_string_pretty(state)
191 .map_err(|e| format!("Failed to serialize checkpoint: {}", e))?;
192
193 let tmp_path = path.with_extension("json.tmp");
195 std::fs::write(&tmp_path, &content)
196 .map_err(|e| format!("Failed to write checkpoint: {}", e))?;
197 std::fs::rename(&tmp_path, &path)
198 .map_err(|e| format!("Failed to finalize checkpoint: {}", e))?;
199
200 Ok(path)
201}
202
203pub fn load_checkpoint(
208 checkpoint_dir: &std::path::Path,
209 session_id: &str,
210) -> Option<CheckpointState> {
211 let path = checkpoint_dir.join(format!("{}.json", session_id));
212
213 match std::fs::read_to_string(&path) {
214 Ok(content) => match serde_json::from_str::<CheckpointState>(&content) {
215 Ok(state) => {
216 info!(
217 session_id = %session_id,
218 iteration = state.iteration,
219 max_iterations = state.max_iterations,
220 "Loaded checkpoint"
221 );
222 Some(state)
223 }
224 Err(e) => {
225 warn!(
226 session_id = %session_id,
227 error = %e,
228 "Failed to deserialize checkpoint"
229 );
230 None
231 }
232 },
233 Err(e) => {
234 if e.kind() != std::io::ErrorKind::NotFound {
235 warn!(
236 session_id = %session_id,
237 error = %e,
238 "Failed to read checkpoint"
239 );
240 }
241 None
242 }
243 }
244}
245
246pub fn delete_checkpoint(checkpoint_dir: &std::path::Path, session_id: &str) {
250 let path = checkpoint_dir.join(format!("{}.json", session_id));
251 if path.exists() {
252 if let Err(e) = std::fs::remove_file(&path) {
253 warn!(
254 session_id = %session_id,
255 error = %e,
256 "Failed to delete checkpoint"
257 );
258 } else {
259 debug!(
260 session_id = %session_id,
261 "Deleted checkpoint"
262 );
263 }
264 }
265}
266
267pub struct AgentLoopConfig {
272 pub max_iterations: usize,
274 pub enable_tools: bool,
276 pub require_approval: bool,
278 pub prompt_injection_protection: bool,
280 pub token_lifetime_secs: u64,
284 pub no_final_required: bool,
286 pub fallback_chain: Option<Arc<std::sync::Mutex<ProviderFallbackChain>>>,
288 pub token_budget: Option<Arc<std::sync::Mutex<TokenBudget>>>,
290 pub ravenfabric: Option<RavenFabricClient>,
292 pub checkpoint_dir: Option<PathBuf>,
296 pub session_id: Option<String>,
299 pub metrics_callback: Option<Box<dyn Fn(u64, u64) + Send + Sync>>,
303
304 pub load_manager: Option<Arc<crate::load::LoadManager>>,
307
308 pub retry_config: Option<RetryConfig>,
313}
314
315impl Default for AgentLoopConfig {
316 fn default() -> Self {
317 Self {
318 max_iterations: 10,
319 enable_tools: false,
320 require_approval: false,
321 prompt_injection_protection: true,
322 token_lifetime_secs: 0,
323 no_final_required: true,
324 fallback_chain: None,
325 token_budget: None,
326 ravenfabric: None,
327 checkpoint_dir: None,
328 session_id: None,
329 metrics_callback: None,
330 load_manager: None,
331 retry_config: None,
332 }
333 }
334}
335
336impl std::fmt::Debug for AgentLoopConfig {
338 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
339 f.debug_struct("AgentLoopConfig")
340 .field("max_iterations", &self.max_iterations)
341 .field("enable_tools", &self.enable_tools)
342 .field("require_approval", &self.require_approval)
343 .field(
344 "prompt_injection_protection",
345 &self.prompt_injection_protection,
346 )
347 .field("token_lifetime_secs", &self.token_lifetime_secs)
348 .field("no_final_required", &self.no_final_required)
349 .field("fallback_chain", &self.fallback_chain)
350 .field("token_budget", &self.token_budget)
351 .field("ravenfabric", &self.ravenfabric)
352 .field("checkpoint_dir", &self.checkpoint_dir)
353 .field("session_id", &self.session_id)
354 .field(
355 "metrics_callback",
356 &self.metrics_callback.as_ref().map(|_| "Box<Fn>"),
357 )
358 .field(
359 "load_manager",
360 &self.load_manager.as_ref().map(|_| "Arc<LoadManager>"),
361 )
362 .finish()
363 }
364}
365
366impl Clone for AgentLoopConfig {
369 fn clone(&self) -> Self {
370 Self {
371 max_iterations: self.max_iterations,
372 enable_tools: self.enable_tools,
373 require_approval: self.require_approval,
374 prompt_injection_protection: self.prompt_injection_protection,
375 token_lifetime_secs: self.token_lifetime_secs,
376 no_final_required: self.no_final_required,
377 fallback_chain: self.fallback_chain.clone(),
378 token_budget: self.token_budget.clone(),
379 ravenfabric: self.ravenfabric.clone(),
380 checkpoint_dir: self.checkpoint_dir.clone(),
381 session_id: self.session_id.clone(),
382 metrics_callback: None,
383 load_manager: self.load_manager.clone(),
384 retry_config: self.retry_config.clone(),
385 }
386 }
387}
388
389#[instrument(skip_all, fields(provider = %llm.provider_name(), model = %llm.model()))]
396pub async fn run_agent_loop(
397 llm: Arc<dyn LLMProviderTrait>,
398 initial_prompt: &str,
399 system_prompt: &str,
400 config: AgentLoopConfig,
401) -> Result<String> {
402 run_agent_loop_with_registry(llm, initial_prompt, system_prompt, config, None).await
403}
404
405#[instrument(skip_all, fields(provider = %llm.provider_name(), model = %llm.model()))]
410pub async fn run_agent_loop_with_registry(
411 llm: Arc<dyn LLMProviderTrait>,
412 initial_prompt: &str,
413 system_prompt: &str,
414 config: AgentLoopConfig,
415 tool_registry: Option<ToolRegistry>,
416) -> Result<String> {
417 let registry = tool_registry.unwrap_or_else(ToolRegistry::with_default_tools);
418 run_agent_loop_inner(
419 llm,
420 initial_prompt,
421 system_prompt,
422 config,
423 registry,
424 "security integration",
425 false,
426 Vec::new(),
427 )
428 .await
429}
430
431#[allow(dead_code)]
436#[instrument(skip_all, fields(provider = %llm.provider_name(), model = %llm.model(), image_count = image_data_uris.len()))]
437pub async fn run_agent_loop_with_images(
438 llm: Arc<dyn LLMProviderTrait>,
439 initial_prompt: &str,
440 system_prompt: &str,
441 config: AgentLoopConfig,
442 tool_registry: Option<ToolRegistry>,
443 image_data_uris: Vec<String>,
444) -> Result<String> {
445 let registry = tool_registry.unwrap_or_else(ToolRegistry::with_default_tools);
446 run_agent_loop_inner(
447 llm,
448 initial_prompt,
449 system_prompt,
450 config,
451 registry,
452 "security integration",
453 false,
454 image_data_uris,
455 )
456 .await
457}
458
459async fn call_llm_with_retry(
475 llm: &Arc<dyn LLMProviderTrait>,
476 messages: Vec<ChatMessage>,
477 retry_config: Option<&RetryConfig>,
478 audit_log: &AuditLog,
479 session_id: &str,
480 checkpoint_dir: &Option<PathBuf>,
481 iteration: usize,
482) -> std::result::Result<crate::llm::ChatResponse, crate::llm::LLMError> {
483 let max_attempts = match retry_config {
484 Some(cfg) => cfg.max_retries + 1, None => 1,
486 };
487
488 let mut last_error = None;
489
490 for attempt in 0..max_attempts {
491 if attempt > 0 {
492 let delay = retry_config.unwrap().delay_for_attempt(attempt - 1);
493 info!(
494 attempt = attempt + 1,
495 max_attempts = max_attempts,
496 delay_ms = delay.as_millis(),
497 iteration = iteration,
498 "Retrying LLM call after transient error"
499 );
500 tokio::time::sleep(delay).await;
501 }
502
503 match llm.chat(messages.clone()).await {
504 Ok(response) => {
505 if attempt > 0 {
506 info!(
507 attempt = attempt + 1,
508 iteration = iteration,
509 "LLM call succeeded on retry"
510 );
511 let _ = audit_log.append(
512 AuditEventType::Custom("Info".to_string()),
513 "llm_retry",
514 &format!("LLM call succeeded on retry attempt {}", attempt + 1),
515 None,
516 );
517 }
518 return Ok(response);
519 }
520 Err(e) => {
521 let is_transient = matches!(
522 &e,
523 crate::llm::LLMError::RequestFailed(_)
524 | crate::llm::LLMError::RateLimited
525 | crate::llm::LLMError::CircuitBreakerOpen(_)
526 );
527
528 if is_transient && attempt + 1 < max_attempts {
529 warn!(
530 error = %e,
531 attempt = attempt + 1,
532 max_attempts = max_attempts,
533 iteration = iteration,
534 "Transient LLM error, will retry"
535 );
536 let _ = audit_log.append(
537 AuditEventType::Error,
538 "llm_retry",
539 &format!("Transient LLM error on attempt {}: {}", attempt + 1, e),
540 None,
541 );
542 last_error = Some(e);
543 continue;
545 }
546
547 if attempt + 1 >= max_attempts && is_transient {
549 warn!(
550 error = %e,
551 attempts = max_attempts,
552 iteration = iteration,
553 "All retry attempts exhausted"
554 );
555 let _ = audit_log.append(
556 AuditEventType::Error,
557 "llm_retry",
558 &format!("All {} retry attempts exhausted: {}", max_attempts, e),
559 None,
560 );
561 }
562
563 if let Some(ref cp_dir) = checkpoint_dir {
565 delete_checkpoint(cp_dir, session_id);
566 }
567 return Err(e);
568 }
569 }
570 }
571
572 Err(last_error.unwrap_or(crate::llm::LLMError::RequestFailed(
574 "All retry attempts exhausted".to_string(),
575 )))
576}
577
578#[allow(clippy::too_many_arguments)]
590#[instrument(skip_all, fields(provider = %llm.provider_name(), model = %llm.model()))]
591async fn run_agent_loop_inner(
592 llm: Arc<dyn LLMProviderTrait>,
593 initial_prompt: &str,
594 system_prompt: &str,
595 config: AgentLoopConfig,
596 registry: ToolRegistry,
597 loop_label: &str,
598 mcp_enabled: bool,
599 image_data_uris: Vec<String>,
600) -> Result<String> {
601 let policy_engine = PolicyEngine::default_secure();
603 let mut sandbox = Sandbox::default();
604 sandbox.init().await.map_err(|e| {
605 crate::error::RavenClawsError::CommandExecution(format!("Sandbox init failed: {}", e))
606 })?;
607 let audit_log = AuditLog::new(format!("agent-{}", std::process::id()));
608
609 let injection_detector = if config.prompt_injection_protection {
611 Some(crate::policy::InjectionDetector::new())
612 } else {
613 None
614 };
615
616 let session_start = std::time::Instant::now();
618
619 info!(
620 provider = llm.provider_name(),
621 model = llm.model(),
622 max_iterations = config.max_iterations,
623 enable_tools = config.enable_tools,
624 tool_count = registry.len(),
625 require_approval = config.require_approval,
626 prompt_injection_protection = config.prompt_injection_protection,
627 token_lifetime_secs = config.token_lifetime_secs,
628 "Agent loop starting with {}",
629 loop_label
630 );
631
632 let _ = audit_log.append(
634 AuditEventType::AgentStart,
635 "agent",
636 &format!(
637 "Agent loop started with {} (model: {})",
638 llm.provider_name(),
639 llm.model()
640 ),
641 Some(serde_json::json!({
642 "provider": llm.provider_name(),
643 "model": llm.model(),
644 "max_iterations": config.max_iterations,
645 "enable_tools": config.enable_tools,
646 "mcp_enabled": mcp_enabled,
647 "tool_count": registry.len(),
648 "require_approval": config.require_approval,
649 "prompt_injection_protection": config.prompt_injection_protection,
650 "token_lifetime_secs": config.token_lifetime_secs,
651 })),
652 );
653
654 let (mut memory, start_iteration) = if let Some(ref checkpoint_dir) = config.checkpoint_dir {
658 if let Some(ref session_id) = config.session_id {
659 if let Some(checkpoint) = load_checkpoint(checkpoint_dir, session_id) {
660 info!(
661 session_id = %session_id,
662 iteration = checkpoint.iteration,
663 max_iterations = checkpoint.max_iterations,
664 "Resuming agent loop from checkpoint"
665 );
666 (
667 ConversationMemory::from_history(checkpoint.messages, 0),
668 checkpoint.iteration + 1, )
670 } else {
671 info!(
672 session_id = %session_id,
673 "No checkpoint found, starting fresh"
674 );
675 let mut m = ConversationMemory::new(system_prompt, 0);
676 if image_data_uris.is_empty() {
677 m.add_user_message(initial_prompt);
678 } else {
679 m.add_user_message_with_images(initial_prompt, image_data_uris.clone());
680 }
681 (m, 0)
682 }
683 } else {
684 let mut m = ConversationMemory::new(system_prompt, 0);
685 if image_data_uris.is_empty() {
686 m.add_user_message(initial_prompt);
687 } else {
688 m.add_user_message_with_images(initial_prompt, image_data_uris.clone());
689 }
690 (m, 0)
691 }
692 } else {
693 let mut m = ConversationMemory::new(system_prompt, 0);
694 if image_data_uris.is_empty() {
695 m.add_user_message(initial_prompt);
696 } else {
697 m.add_user_message_with_images(initial_prompt, image_data_uris.clone());
698 }
699 (m, 0)
700 };
701
702 let session_id = config
704 .session_id
705 .clone()
706 .unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
707
708 for iteration in start_iteration..config.max_iterations {
709 if config.token_lifetime_secs > 0 {
711 let elapsed = session_start.elapsed().as_secs();
712 if elapsed >= config.token_lifetime_secs {
713 warn!(
714 iteration = iteration,
715 elapsed_secs = elapsed,
716 token_lifetime_secs = config.token_lifetime_secs,
717 "Agent loop reached token lifetime limit"
718 );
719 let _ = audit_log.append(
720 AuditEventType::SecurityViolation,
721 "token_lifetime",
722 &format!(
723 "Session expired after {} seconds (limit: {}s)",
724 elapsed, config.token_lifetime_secs
725 ),
726 Some(serde_json::json!({
727 "elapsed_secs": elapsed,
728 "token_lifetime_secs": config.token_lifetime_secs,
729 "iteration": iteration,
730 })),
731 );
732 if let Some(ref checkpoint_dir) = config.checkpoint_dir {
734 delete_checkpoint(checkpoint_dir, &session_id);
735 }
736 return Err(crate::error::RavenClawsError::SecurityViolation(format!(
737 "Session token expired after {} seconds (limit: {}s)",
738 elapsed, config.token_lifetime_secs
739 )));
740 }
741 }
742 let messages = memory.history().to_vec();
743
744 if let Some(ref budget) = config.token_budget {
746 let budget = budget.lock().unwrap();
747 if budget.remaining() < 100 {
748 warn!(
749 iteration = iteration,
750 remaining = budget.remaining(),
751 "Token budget exhausted"
752 );
753 let _ = audit_log.append(
754 AuditEventType::SecurityViolation,
755 "token_budget",
756 &format!("Token budget exhausted (remaining: {})", budget.remaining()),
757 Some(serde_json::json!({
758 "remaining": budget.remaining(),
759 "used": budget.used_tokens,
760 "iteration": iteration,
761 })),
762 );
763 if let Some(ref checkpoint_dir) = config.checkpoint_dir {
765 delete_checkpoint(checkpoint_dir, &session_id);
766 }
767 return Err(crate::error::RavenClawsError::SecurityViolation(
768 "Token budget exhausted".to_string(),
769 ));
770 }
771 }
772
773 if let Some(ref load_manager) = config.load_manager {
775 let admission = load_manager.check_admission();
776 if !admission.is_allowed() {
777 warn!(
778 ?admission,
779 iteration = iteration,
780 "Admission denied before LLM call"
781 );
782 let _ = audit_log.append(
783 AuditEventType::Error,
784 "load_manager",
785 &format!("Admission denied: {:?}", admission),
786 None,
787 );
788 load_manager.record_outcome(crate::load::RequestOutcome::Failure);
789 if let Some(ref checkpoint_dir) = config.checkpoint_dir {
791 delete_checkpoint(checkpoint_dir, &session_id);
792 }
793 return Err(crate::error::RavenClawsError::SecurityViolation(format!(
794 "Admission denied: {:?}",
795 admission
796 )));
797 }
798 }
799
800 let response = match call_llm_with_retry(
804 &llm,
805 messages.clone(),
806 config.retry_config.as_ref(),
807 &audit_log,
808 &session_id,
809 &config.checkpoint_dir,
810 iteration,
811 )
812 .await
813 {
814 Ok(r) => {
815 if let Some(ref load_manager) = config.load_manager {
817 load_manager.record_outcome(crate::load::RequestOutcome::Success);
818 }
819 r
820 }
821 Err(e) => {
822 if let Some(ref load_manager) = config.load_manager {
824 load_manager.record_outcome(crate::load::RequestOutcome::Failure);
825 }
826 if let Some(ref chain) = config.fallback_chain {
828 warn!(error = %e, "Primary LLM failed after retries, trying fallback chain");
829 let _ = audit_log.append(
830 AuditEventType::Error,
831 "llm",
832 &format!("Primary LLM failed after retries, trying fallback: {}", e),
833 None,
834 );
835 let configs = {
837 let c = chain.lock().unwrap();
838 c.configs.clone()
839 };
840 let mut temp_chain = ProviderFallbackChain::new(configs);
841 match temp_chain.chat_with_fallback(messages).await {
842 Ok(r) => {
843 info!("Fallback chain succeeded");
844 if let Some(ref budget) = config.token_budget {
846 if let Some(usage) = &r.usage {
847 let mut b = budget.lock().unwrap();
848 b.record_usage(usage.total_tokens);
849 }
850 }
851 r
852 }
853 Err(fallback_e) => {
854 warn!(error = %fallback_e, "Fallback chain also failed");
855 let _ = audit_log.append(
856 AuditEventType::Error,
857 "llm",
858 &format!("All providers failed: {}", fallback_e),
859 None,
860 );
861 return Err(crate::error::RavenClawsError::Llm(fallback_e));
863 }
864 }
865 } else {
866 warn!(error = %e, "LLM request failed after retries");
867 let _ = audit_log.append(
868 AuditEventType::Error,
869 "llm",
870 &format!("LLM request failed after retries: {}", e),
871 None,
872 );
873 return Err(crate::error::RavenClawsError::Llm(e));
875 }
876 }
877 };
878
879 let mut iteration_tokens: u64 = 0;
881 if let Some(ref budget) = config.token_budget {
882 if let Some(usage) = &response.usage {
883 let mut b = budget.lock().unwrap();
884 b.record_usage(usage.total_tokens);
885 iteration_tokens = usage.total_tokens as u64;
886 debug!(
887 iteration = iteration,
888 tokens_used = usage.total_tokens,
889 total_used = b.used_tokens,
890 remaining = b.remaining(),
891 "Token usage recorded"
892 );
893 }
894 } else if let Some(usage) = &response.usage {
895 iteration_tokens = usage.total_tokens as u64;
896 }
897
898 if let Some(ref cb) = config.metrics_callback {
900 cb(iteration_tokens, 0);
901 }
902
903 if let Some(ref rf) = config.ravenfabric {
905 if rf.is_enabled() {
906 let _ = rf.health().await;
907 info!(
908 iteration = iteration,
909 ravenfabric = true,
910 "RavenFabric health check completed"
911 );
912 }
913 }
914
915 let first_choice = response.choices.first();
916 let content = first_choice
917 .map(|c| c.message.content.clone())
918 .unwrap_or_default();
919
920 debug!(
921 iteration = iteration,
922 response_length = content.len(),
923 response_preview = %content[..content.len().min(500)],
924 "LLM response received"
925 );
926
927 if let Some(ref detector) = injection_detector {
929 match detector.check(&content) {
930 crate::policy::InjectionVerdict::Suspicious(reason) => {
931 warn!(
932 iteration = iteration,
933 reason = %reason,
934 "Prompt-injection detected in LLM response"
935 );
936 let _ = audit_log.append(
937 AuditEventType::SecurityViolation,
938 "injection_detector",
939 &format!("Prompt-injection detected: {}", reason),
940 Some(serde_json::json!({
941 "reason": reason,
942 "iteration": iteration,
943 "content_preview": &content[..content.len().min(200)],
944 })),
945 );
946 if let Some(ref checkpoint_dir) = config.checkpoint_dir {
948 delete_checkpoint(checkpoint_dir, &session_id);
949 }
950 return Err(crate::error::RavenClawsError::SecurityViolation(format!(
951 "LLM response blocked: potential prompt injection ({})",
952 reason
953 )));
954 }
955 crate::policy::InjectionVerdict::Clean => {}
956 }
957 }
958
959 if config.enable_tools {
961 if let Some((tool_name, args)) = first_choice.and_then(parse_structured_tool_call) {
962 info!(tool = %tool_name, "Structured tool call detected");
963
964 if let Some(tool_result) = execute_parsed_tool_call(
966 tool_name,
967 args,
968 ®istry,
969 &policy_engine,
970 &sandbox,
971 &audit_log,
972 config.require_approval,
973 )
974 .await
975 {
976 let observation = if tool_result.success {
977 format!("OBSERVATION: {}", tool_result.output)
978 } else {
979 format!(
980 "OBSERVATION: Tool failed with error: {}",
981 tool_result.error.as_deref().unwrap_or("unknown error")
982 )
983 };
984
985 memory.add_user_message(&observation);
986
987 if let Some(ref cb) = config.metrics_callback {
989 cb(0, 1);
990 }
991
992 info!(
993 iteration = iteration,
994 tool = %tool_result.tool_name,
995 success = tool_result.success,
996 "Structured tool executed"
997 );
998 continue;
999 }
1000 }
1001 }
1002
1003 if content.contains("FINAL:") {
1005 let final_response = content
1006 .split("FINAL:")
1007 .nth(1)
1008 .unwrap_or("")
1009 .trim()
1010 .to_string();
1011
1012 memory.add_assistant_message(&content);
1013
1014 let _ = audit_log.append(
1016 AuditEventType::AgentFinish,
1017 "agent",
1018 "Agent loop completed successfully",
1019 Some(serde_json::json!({
1020 "iterations": iteration + 1,
1021 "final_response_length": final_response.len(),
1022 })),
1023 );
1024
1025 if let Some(ref checkpoint_dir) = config.checkpoint_dir {
1027 delete_checkpoint(checkpoint_dir, &session_id);
1028 }
1029
1030 return Ok(final_response);
1031 }
1032
1033 if config.enable_tools {
1035 if let Some(tool_result) = execute_tool_call_with_security(
1036 &content,
1037 ®istry,
1038 &policy_engine,
1039 &sandbox,
1040 &audit_log,
1041 )
1042 .await
1043 {
1044 let observation = if tool_result.success {
1045 format!("OBSERVATION: {}", tool_result.output)
1046 } else {
1047 format!(
1048 "OBSERVATION: Tool failed with error: {}",
1049 tool_result.error.as_deref().unwrap_or("unknown error")
1050 )
1051 };
1052
1053 memory.add_assistant_message(&content);
1054 memory.add_user_message(&observation);
1055
1056 if let Some(ref cb) = config.metrics_callback {
1058 cb(0, 1);
1059 }
1060
1061 info!(
1062 iteration = iteration,
1063 tool = %tool_result.tool_name,
1064 success = tool_result.success,
1065 "Tool executed"
1066 );
1067 continue;
1068 }
1069 }
1070
1071 memory.add_assistant_message(&content);
1073
1074 if let Some(ref checkpoint_dir) = config.checkpoint_dir {
1076 let checkpoint = CheckpointState::new(
1077 session_id.clone(),
1078 iteration,
1079 config.max_iterations,
1080 memory.history().to_vec(),
1081 initial_prompt,
1082 system_prompt,
1083 llm.provider_name(),
1084 llm.model(),
1085 config.enable_tools,
1086 );
1087 if let Err(e) = save_checkpoint(checkpoint_dir, &checkpoint) {
1088 warn!(
1089 session_id = %session_id,
1090 iteration = iteration,
1091 error = %e,
1092 "Failed to save checkpoint"
1093 );
1094 } else {
1095 debug!(
1096 session_id = %session_id,
1097 iteration = iteration,
1098 "Checkpoint saved"
1099 );
1100 }
1101 }
1102
1103 if config.no_final_required {
1105 info!(
1106 iteration = iteration,
1107 response_length = content.len(),
1108 "no_final_required: treating response as completion"
1109 );
1110 let _ = audit_log.append(
1111 AuditEventType::AgentFinish,
1112 "agent",
1113 "Agent loop completed (no_final_required)",
1114 Some(serde_json::json!({
1115 "iterations": iteration + 1,
1116 "final_response_length": content.len(),
1117 })),
1118 );
1119 if let Some(ref checkpoint_dir) = config.checkpoint_dir {
1121 delete_checkpoint(checkpoint_dir, &session_id);
1122 }
1123 return Ok(content);
1124 }
1125
1126 info!(
1127 iteration = iteration,
1128 thought = %content.lines().find(|l| l.starts_with("THOUGHT:")).unwrap_or("<no thought>"),
1129 "Agent loop progress"
1130 );
1131 }
1132
1133 warn!(
1135 max_iterations = config.max_iterations,
1136 "Agent loop reached max iterations"
1137 );
1138
1139 let _ = audit_log.append(
1140 AuditEventType::Error,
1141 "agent",
1142 "Agent loop reached max iterations without completing",
1143 Some(serde_json::json!({
1144 "max_iterations": config.max_iterations,
1145 })),
1146 );
1147
1148 if let Some(ref checkpoint_dir) = config.checkpoint_dir {
1150 delete_checkpoint(checkpoint_dir, &session_id);
1151 }
1152
1153 let history = memory.history();
1154 if history.len() > 1 {
1155 if let Some(last) = history.last() {
1156 return Ok(last.content.clone());
1157 }
1158 }
1159
1160 Err(crate::error::RavenClawsError::CommandExecution(
1161 "Agent loop reached max iterations without completing the task".to_string(),
1162 ))
1163}
1164
1165#[allow(dead_code)]
1171#[instrument(skip_all, fields(provider = %llm.provider_name(), model = %llm.model()))]
1172pub async fn run_agent_loop_with_mcp(
1173 llm: Arc<dyn LLMProviderTrait>,
1174 initial_prompt: &str,
1175 system_prompt: &str,
1176 config: AgentLoopConfig,
1177 mcp_client: Option<Arc<RwLock<McpClient>>>,
1178) -> Result<String> {
1179 run_agent_loop_with_mcp_and_registry(
1180 llm,
1181 initial_prompt,
1182 system_prompt,
1183 config,
1184 mcp_client,
1185 None,
1186 )
1187 .await
1188}
1189
1190#[instrument(skip_all, fields(provider = %llm.provider_name(), model = %llm.model()))]
1192pub async fn run_agent_loop_with_mcp_and_registry(
1193 llm: Arc<dyn LLMProviderTrait>,
1194 initial_prompt: &str,
1195 system_prompt: &str,
1196 config: AgentLoopConfig,
1197 mcp_client: Option<Arc<RwLock<McpClient>>>,
1198 tool_registry: Option<ToolRegistry>,
1199) -> Result<String> {
1200 let mut registry = tool_registry.unwrap_or_else(ToolRegistry::with_default_tools);
1202
1203 if let Some(client) = &mcp_client {
1205 match crate::mcp::register_mcp_tools(&mut registry, client.clone()).await {
1206 Ok(count) => {
1207 info!(count, "MCP tools registered");
1208 }
1209 Err(e) => {
1210 warn!(error = %e, "Failed to register MCP tools");
1211 }
1212 }
1213 }
1214
1215 let mcp_enabled = mcp_client.is_some();
1216 run_agent_loop_inner(
1217 llm,
1218 initial_prompt,
1219 system_prompt,
1220 config,
1221 registry,
1222 "MCP integration",
1223 mcp_enabled,
1224 Vec::new(),
1225 )
1226 .await
1227}
1228
1229#[instrument(skip_all, fields(provider = %llm.provider_name(), model = %llm.model(), image_count = image_data_uris.len()))]
1231pub async fn run_agent_loop_with_mcp_and_images(
1232 llm: Arc<dyn LLMProviderTrait>,
1233 initial_prompt: &str,
1234 system_prompt: &str,
1235 config: AgentLoopConfig,
1236 mcp_client: Option<Arc<RwLock<McpClient>>>,
1237 tool_registry: Option<ToolRegistry>,
1238 image_data_uris: Vec<String>,
1239) -> Result<String> {
1240 let mut registry = tool_registry.unwrap_or_else(ToolRegistry::with_default_tools);
1241
1242 if let Some(client) = &mcp_client {
1243 match crate::mcp::register_mcp_tools(&mut registry, client.clone()).await {
1244 Ok(count) => {
1245 info!(count, "MCP tools registered");
1246 }
1247 Err(e) => {
1248 warn!(error = %e, "Failed to register MCP tools");
1249 }
1250 }
1251 }
1252
1253 let mcp_enabled = mcp_client.is_some();
1254 run_agent_loop_inner(
1255 llm,
1256 initial_prompt,
1257 system_prompt,
1258 config,
1259 registry,
1260 "MCP integration",
1261 mcp_enabled,
1262 image_data_uris,
1263 )
1264 .await
1265}
1266
1267async fn prompt_for_approval(tool_name: &str, args: &serde_json::Value) -> bool {
1272 use std::io::{IsTerminal, Write};
1273
1274 let args_str = serde_json::to_string_pretty(args).unwrap_or_default();
1275
1276 if !std::io::stdin().is_terminal() {
1278 warn!(
1279 tool = %tool_name,
1280 "stdin is not a TTY — auto-approving tool call (use --require-approval only in interactive mode)"
1281 );
1282 return true;
1283 }
1284
1285 eprintln!("\n⚠️ Tool requires approval:");
1287 eprintln!(" Tool: {}", tool_name);
1288 for line in args_str.lines() {
1289 eprintln!(" {}", line);
1290 }
1291 eprint!(" Approve? [y/N] ");
1292 std::io::stderr().flush().ok();
1293
1294 let mut input = String::new();
1295 match std::io::stdin().read_line(&mut input) {
1296 Ok(_) => {
1297 let trimmed = input.trim().to_lowercase();
1298 trimmed == "y" || trimmed == "yes"
1299 }
1300 Err(e) => {
1301 warn!(error = %e, "Failed to read approval input — denying by default");
1302 false
1303 }
1304 }
1305}
1306
1307#[cfg(test)]
1310async fn prompt_for_approval_with_input(
1311 tool_name: &str,
1312 args: &serde_json::Value,
1313 input: &str,
1314) -> bool {
1315 use std::io::Write;
1316
1317 let args_str = serde_json::to_string_pretty(args).unwrap_or_default();
1318
1319 eprintln!("\n⚠️ Tool requires approval:");
1320 eprintln!(" Tool: {}", tool_name);
1321 for line in args_str.lines() {
1322 eprintln!(" {}", line);
1323 }
1324 eprint!(" Approve? [y/N] ");
1325 std::io::stderr().flush().ok();
1326
1327 let trimmed = input.trim().to_lowercase();
1328 trimmed == "y" || trimmed == "yes"
1329}
1330
1331async fn execute_parsed_tool_call(
1340 tool_name: String,
1341 args: serde_json::Value,
1342 registry: &ToolRegistry,
1343 policy_engine: &PolicyEngine,
1344 _sandbox: &Sandbox,
1345 audit_log: &AuditLog,
1346 require_approval: bool,
1347) -> Option<ToolResult> {
1348 info!(tool = %tool_name, "Executing parsed tool call");
1349
1350 let _ = audit_log.tool_call(&tool_name, &args);
1352
1353 if require_approval && policy_engine.requires_approval(&tool_name) {
1355 let _ = audit_log.append(
1356 AuditEventType::ApprovalRequested,
1357 "approval",
1358 &format!("Approval required for tool: {}", tool_name),
1359 Some(serde_json::json!({"tool": tool_name, "args": args})),
1360 );
1361
1362 let granted = prompt_for_approval(&tool_name, &args).await;
1364
1365 if !granted {
1366 let _ = audit_log.approval(&tool_name, false, Some("Denied by user"));
1367 warn!(tool = %tool_name, "Tool call denied by user");
1368 return Some(ToolResult {
1369 tool_name: tool_name.clone(),
1370 success: false,
1371 output: String::new(),
1372 error: Some(format!("Approval denied by user for tool: {}", tool_name)),
1373 exit_code: Some(-1),
1374 duration_ms: None,
1375 });
1376 }
1377
1378 let _ = audit_log.approval(&tool_name, true, Some("Approved by user"));
1379 info!(tool = %tool_name, "Tool call approved by user");
1380 }
1381
1382 let policy_decision = policy_engine.check_tool_call(&tool_name, &args);
1384
1385 match &policy_decision {
1387 Decision::Allow => {
1388 let _ = audit_log.policy_decision(&tool_name, true, None);
1389 }
1390 Decision::Deny(reason) => {
1391 let _ = audit_log.policy_decision(&tool_name, false, Some(reason));
1392 warn!(tool = %tool_name, reason = %reason, "Tool call denied by policy");
1393 return Some(ToolResult {
1394 tool_name: tool_name.clone(),
1395 success: false,
1396 output: String::new(),
1397 error: Some(format!("Policy denied: {}", reason)),
1398 exit_code: Some(-1),
1399 duration_ms: None,
1400 });
1401 }
1402 }
1403
1404 let tool_name_clone = tool_name.clone();
1406 let call = ToolCall {
1407 name: tool_name.clone(),
1408 arguments: args,
1409 id: None,
1410 };
1411
1412 let result = match registry.execute(call).await {
1413 Ok(result) => {
1414 let _ = audit_log.append(
1416 AuditEventType::ToolResult,
1417 &tool_name_clone,
1418 &format!(
1419 "Tool executed: {} (success: {})",
1420 tool_name_clone, result.success
1421 ),
1422 Some(serde_json::json!({
1423 "success": result.success,
1424 "exit_code": result.exit_code,
1425 "duration_ms": result.duration_ms,
1426 })),
1427 );
1428 result
1429 }
1430 Err(e) => {
1431 let _ = audit_log.append(
1433 AuditEventType::Error,
1434 &tool_name_clone,
1435 &format!("Tool execution failed: {}", e),
1436 None,
1437 );
1438 ToolResult {
1439 tool_name: tool_name_clone,
1440 success: false,
1441 output: String::new(),
1442 error: Some(e.to_string()),
1443 exit_code: Some(-1),
1444 duration_ms: None,
1445 }
1446 }
1447 };
1448
1449 Some(result)
1450}
1451
1452async fn execute_tool_call_with_security(
1461 content: &str,
1462 registry: &ToolRegistry,
1463 policy_engine: &PolicyEngine,
1464 _sandbox: &Sandbox,
1465 audit_log: &AuditLog,
1466) -> Option<ToolResult> {
1467 let (tool_name, args) = parse_tool_call(content)?;
1469
1470 execute_parsed_tool_call(
1472 tool_name,
1473 args,
1474 registry,
1475 policy_engine,
1476 _sandbox,
1477 audit_log,
1478 false, )
1480 .await
1481}
1482
1483fn parse_structured_tool_call(choice: &Choice) -> Option<(String, serde_json::Value)> {
1487 let tool_calls = choice.tool_calls.as_ref()?;
1488 let first_call = tool_calls.first()?;
1489
1490 let tool_name = first_call.function.name.clone();
1491 let args: serde_json::Value = serde_json::from_str(&first_call.function.arguments).ok()?;
1492
1493 Some((tool_name, args))
1494}
1495
1496fn parse_tool_call(content: &str) -> Option<(String, serde_json::Value)> {
1498 let mut lines = content.lines();
1499 let tool_call_line = lines.find(|l| l.trim().starts_with("TOOL_CALL:"))?;
1500
1501 let tool_name = tool_call_line
1502 .trim()
1503 .strip_prefix("TOOL_CALL:")
1504 .map(|s| s.trim())
1505 .filter(|s| !s.is_empty())?
1506 .to_string();
1507
1508 let args_line = lines.find(|l| l.trim().starts_with("ARGS:"))?;
1510 let args_str = args_line.trim().strip_prefix("ARGS:").map(|s| s.trim())?;
1511
1512 let args: serde_json::Value = serde_json::from_str(args_str).ok()?;
1513
1514 Some((tool_name, args))
1515}
1516
1517pub async fn run_single(
1519 llm: Arc<dyn LLMProviderTrait>,
1520 config: Config,
1521 ravenfabric: Option<RavenFabricClient>,
1522) -> Result<()> {
1523 info!(
1524 "Starting single agent mode with provider: {}",
1525 llm.provider_name()
1526 );
1527
1528 if let Some(ref rf) = ravenfabric {
1530 if rf.is_enabled() {
1531 info!("RavenFabric remote execution available");
1532 match rf.health().await {
1533 Ok(true) => info!("RavenFabric mesh is healthy"),
1534 Ok(false) => warn!("RavenFabric mesh returned unhealthy status"),
1535 Err(e) => warn!(error = %e, "RavenFabric health check failed"),
1536 }
1537 }
1538 }
1539
1540 let system_prompt = &config.llm.system_prompt;
1541
1542 let messages = vec![
1543 ChatMessage::new("system", system_prompt.to_string()),
1544 ChatMessage::new("user", "Ready. Awaiting instructions."),
1545 ];
1546
1547 match llm.chat(messages).await {
1548 Ok(response) => {
1549 if let Some(choice) = response.choices.first() {
1550 info!(provider = llm.provider_name(), model = llm.model(), response = %choice.message.content, "Agent response received");
1551
1552 if let Some(ref rf) = ravenfabric {
1554 if rf.is_enabled() {
1555 let preview = choice.message.content.chars().take(500).collect::<String>();
1556 let _ = rf.broadcast(&preview, 30).await;
1557 info!("Agent result broadcast to RavenFabric mesh");
1558 }
1559 }
1560 }
1561 }
1562 Err(e) => {
1563 warn!(error = %e, provider = llm.provider_name(), "LLM request failed");
1564 }
1565 }
1566
1567 Ok(())
1568}
1569
1570pub async fn run_swarm(
1575 llm: Arc<dyn LLMProviderTrait>,
1576 config: Config,
1577 ravenfabric: Option<RavenFabricClient>,
1578) -> Result<()> {
1579 info!("Starting swarm mode (single-provider) — 3 parallel agents");
1580
1581 if let Some(ref rf) = ravenfabric {
1583 if rf.is_enabled() {
1584 info!("RavenFabric remote execution available for swarm coordination");
1585 match rf.health().await {
1586 Ok(true) => info!("RavenFabric mesh is healthy"),
1587 Ok(false) => warn!("RavenFabric mesh returned unhealthy status"),
1588 Err(e) => warn!(error = %e, "RavenFabric health check failed"),
1589 }
1590 }
1591 }
1592
1593 let _system_prompt = &config.llm.system_prompt;
1594 let num_agents = 3;
1595 let mut handles = Vec::new();
1596
1597 let personas = [
1599 "You are an analytical agent. Focus on logic, structure, and precision.",
1600 "You are a creative agent. Focus on innovation, alternatives, and possibilities.",
1601 "You are a pragmatic agent. Focus on simplicity, efficiency, and practicality.",
1602 ];
1603
1604 for (i, persona) in personas.iter().enumerate().take(num_agents) {
1605 let llm_clone = llm.clone();
1606 let persona = persona.to_string();
1607 let task = "Analyze the given task and provide your solution.".to_string();
1608
1609 let handle = tokio::spawn(async move {
1610 let mut memory = ConversationMemory::new(&persona, 10);
1611 memory.add_user_message(&task);
1612
1613 let messages = memory.history().to_vec();
1614 match llm_clone.chat(messages).await {
1615 Ok(response) => {
1616 let content = response
1617 .choices
1618 .first()
1619 .map(|c| c.message.content.clone())
1620 .unwrap_or_default();
1621 Ok((i, content))
1622 }
1623 Err(e) => Err(format!("Agent {} failed: {}", i, e)),
1624 }
1625 });
1626
1627 handles.push(handle);
1628 }
1629
1630 let mut results: Vec<(usize, String)> = Vec::new();
1632 for handle in handles {
1633 match handle.await {
1634 Ok(Ok((idx, result))) => {
1635 info!("Agent {} completed: {} chars", idx, result.len());
1636 results.push((idx, result));
1637 }
1638 Ok(Err(e)) => warn!("Agent failed: {}", e),
1639 Err(e) => warn!("Agent join failed: {}", e),
1640 }
1641 }
1642
1643 println!("\n🐦⬛ Swarm Results ({} agents):", results.len());
1645 for (idx, result) in &results {
1646 println!(
1647 "\n── Agent {} ({}) ──",
1648 idx + 1,
1649 personas[*idx].split('.').next().unwrap_or("Unknown")
1650 );
1651 println!("{}", result);
1652 }
1653
1654 if let Some(ref rf) = ravenfabric {
1656 if rf.is_enabled() {
1657 let summary = format!(
1658 "Swarm completed: {} agents, results: {}",
1659 results.len(),
1660 results
1661 .iter()
1662 .map(|(i, r)| format!("Agent {}: {} chars", i, r.len()))
1663 .collect::<Vec<_>>()
1664 .join(", ")
1665 );
1666 let _ = rf.broadcast(&summary, 30).await;
1667 info!("Swarm results broadcast to RavenFabric mesh");
1668 }
1669 }
1670
1671 Ok(())
1672}
1673
1674pub async fn run_supervisor(
1679 llm: Arc<dyn LLMProviderTrait>,
1680 config: Config,
1681 ravenfabric: Option<RavenFabricClient>,
1682) -> Result<()> {
1683 info!("Starting supervisor mode (single-provider)");
1684
1685 if let Some(ref rf) = ravenfabric {
1687 if rf.is_enabled() {
1688 info!("RavenFabric remote execution available for supervisor coordination");
1689 match rf.health().await {
1690 Ok(true) => info!("RavenFabric mesh is healthy"),
1691 Ok(false) => warn!("RavenFabric mesh returned unhealthy status"),
1692 Err(e) => warn!(error = %e, "RavenFabric health check failed"),
1693 }
1694 }
1695 }
1696
1697 let system_prompt = &config.llm.system_prompt;
1698 let policy_engine = PolicyEngine::default_secure();
1699 let mut sandbox = Sandbox::default();
1700 sandbox.init().await.map_err(|e| {
1701 crate::error::RavenClawsError::CommandExecution(format!("Sandbox init failed: {}", e))
1702 })?;
1703 let audit_log = AuditLog::new(format!("supervisor-{}", std::process::id()));
1704 let registry = ToolRegistry::with_default_tools();
1705
1706 let supervisor_prompt = format!(
1708 "You are a supervisor agent. Your task is to decompose complex tasks into subtasks \
1709 and coordinate sub-agents to complete them. \
1710 \n\nFor each subtask, respond with:\n\
1711 SUBTASK: <description>\n\
1712 AGENT: <agent_number>\n\
1713 \nWhen all subtasks are complete, respond with:\n\
1714 FINAL: <aggregated result>\n\
1715 \nTask: {}",
1716 "Coordinate the completion of the assigned task."
1717 );
1718
1719 let mut memory = ConversationMemory::new(system_prompt, 20);
1720 memory.add_user_message(&supervisor_prompt);
1721
1722 let mut subtask_results: Vec<String> = Vec::new();
1723 let mut iteration = 0;
1724 let max_iterations = 15;
1725
1726 loop {
1727 iteration += 1;
1728 if iteration > max_iterations {
1729 warn!("Supervisor reached max iterations");
1730 break;
1731 }
1732
1733 let messages = memory.history().to_vec();
1734 let response = match llm.chat(messages).await {
1735 Ok(r) => r,
1736 Err(e) => {
1737 warn!(error = %e, "Supervisor LLM request failed");
1738 continue;
1739 }
1740 };
1741
1742 let content = response
1743 .choices
1744 .first()
1745 .map(|c| c.message.content.clone())
1746 .unwrap_or_default();
1747
1748 if content.contains("FINAL:") {
1750 let final_response = content
1751 .split("FINAL:")
1752 .nth(1)
1753 .unwrap_or("")
1754 .trim()
1755 .to_string();
1756 info!("Supervisor completed task: {} chars", final_response.len());
1757
1758 let _ = audit_log.append(
1759 AuditEventType::AgentFinish,
1760 "supervisor",
1761 "Supervisor completed task coordination",
1762 Some(serde_json::json!({
1763 "iterations": iteration,
1764 "subtasks_completed": subtask_results.len(),
1765 })),
1766 );
1767
1768 println!("\n🐦⬛ Supervisor Result:\n{}", final_response);
1769 return Ok(());
1770 }
1771
1772 if content.contains("SUBTASK:") {
1774 let subtask_block = content.split("SUBTASK:").nth(1).unwrap_or("");
1775 let subtask_lines: Vec<&str> = subtask_block.lines().take(3).collect();
1776
1777 let subtask_desc = subtask_lines.first().unwrap_or(&"").trim();
1778 let agent_num = subtask_lines
1779 .iter()
1780 .find(|l| l.starts_with("AGENT:"))
1781 .and_then(|l| l.split(':').nth(1))
1782 .unwrap_or("1")
1783 .trim();
1784
1785 if !subtask_desc.is_empty() {
1786 info!("Subtask {}: {}", agent_num, subtask_desc);
1787
1788 let subtask_result = run_subtask_agent(
1790 llm.clone(),
1791 subtask_desc,
1792 system_prompt,
1793 &policy_engine,
1794 &sandbox,
1795 &audit_log,
1796 ®istry,
1797 )
1798 .await;
1799
1800 match subtask_result {
1801 Ok(result) => {
1802 info!("Subtask {} completed: {} chars", agent_num, result.len());
1803 subtask_results.push(format!("Agent {} result: {}", agent_num, result));
1804
1805 memory.add_assistant_message(&format!(
1806 "Decomposed subtask {}: {}",
1807 agent_num, subtask_desc
1808 ));
1809 memory
1810 .add_user_message(&format!("Subtask {} result: {}", agent_num, result));
1811 }
1812 Err(e) => {
1813 warn!("Subtask {} failed: {}", agent_num, e);
1814 memory
1815 .add_assistant_message(&format!("Subtask {} failed: {}", agent_num, e));
1816 }
1817 }
1818 }
1819 } else {
1820 memory.add_assistant_message(&content);
1821 }
1822 }
1823
1824 if !subtask_results.is_empty() {
1826 let aggregated = subtask_results.join("\n\n");
1827 info!(
1828 "Supervisor aggregated {} subtask results",
1829 subtask_results.len()
1830 );
1831
1832 if let Some(ref rf) = ravenfabric {
1834 if rf.is_enabled() {
1835 let summary = format!(
1836 "Supervisor completed: {} subtasks, result: {} chars",
1837 subtask_results.len(),
1838 aggregated.len()
1839 );
1840 let _ = rf.broadcast(&summary, 30).await;
1841 info!("Supervisor result broadcast to RavenFabric mesh");
1842 }
1843 }
1844
1845 println!("\n🐦⬛ Supervisor Aggregated Result:\n{}", aggregated);
1846 return Ok(());
1847 }
1848
1849 Err(crate::error::RavenClawsError::CommandExecution(
1850 "Supervisor mode completed without results".to_string(),
1851 ))
1852}
1853
1854async fn run_subtask_agent(
1856 llm: Arc<dyn LLMProviderTrait>,
1857 subtask: &str,
1858 system_prompt: &str,
1859 policy_engine: &PolicyEngine,
1860 sandbox: &Sandbox,
1861 audit_log: &AuditLog,
1862 registry: &ToolRegistry,
1863) -> Result<String> {
1864 let mut memory = ConversationMemory::new(system_prompt, 10);
1865 memory.add_user_message(&format!("Execute this subtask: {}", subtask));
1866
1867 for i in 0..5 {
1868 let messages = memory.history().to_vec();
1869 let response = match llm.chat(messages).await {
1870 Ok(r) => r,
1871 Err(e) => {
1872 warn!(error = %e, iteration = i, "Subtask agent LLM failed");
1873 continue;
1874 }
1875 };
1876
1877 let content = response
1878 .choices
1879 .first()
1880 .map(|c| c.message.content.clone())
1881 .unwrap_or_default();
1882
1883 if content.contains("FINAL:") || content.contains("DONE:") {
1884 return Ok(content
1885 .replace("FINAL:", "")
1886 .replace("DONE:", "")
1887 .trim()
1888 .to_string());
1889 }
1890
1891 if let Some(tool_result) =
1893 execute_tool_call_with_security(&content, registry, policy_engine, sandbox, audit_log)
1894 .await
1895 {
1896 memory.add_assistant_message(&content);
1897 memory.add_user_message(&format!("Tool result: {}", tool_result.output));
1898 } else {
1899 memory.add_assistant_message(&content);
1900 memory.add_user_message("Continue with next step.");
1901 }
1902 }
1903
1904 Ok("Subtask completed".to_string())
1905}
1906
1907pub async fn run_single_multi(
1909 multi_llm: MultiModelManager,
1910 config: Config,
1911 ravenfabric: Option<RavenFabricClient>,
1912) -> Result<()> {
1913 info!(
1914 "Starting single agent mode (multi-model) with {} providers",
1915 multi_llm.client_count()
1916 );
1917
1918 if let Some(ref rf) = ravenfabric {
1920 if rf.is_enabled() {
1921 info!("RavenFabric remote execution available");
1922 match rf.health().await {
1923 Ok(true) => info!("RavenFabric mesh is healthy"),
1924 Ok(false) => warn!("RavenFabric mesh returned unhealthy status"),
1925 Err(e) => warn!(error = %e, "RavenFabric health check failed"),
1926 }
1927 }
1928 }
1929
1930 let system_prompt = &config.llm.system_prompt;
1931
1932 let messages = vec![
1933 ChatMessage::new("system", system_prompt.to_string()),
1934 ChatMessage::new("user", "Ready. Awaiting instructions."),
1935 ];
1936
1937 let mut last_index = 0;
1939 for i in 0..multi_llm.client_count() {
1940 let client = if i == 0 {
1941 multi_llm.get_client(0)
1942 } else {
1943 multi_llm.next_client(last_index)
1944 };
1945
1946 if let Some(client) = client {
1947 match client.chat(messages.clone()).await {
1948 Ok(response) => {
1949 if let Some(choice) = response.choices.first() {
1950 info!(provider = client.provider_name(), model = client.model(), response = %choice.message.content, "Provider response received");
1951 }
1952 }
1953 Err(e) => {
1954 warn!(error = %e, provider = client.provider_name(), model = client.model(), "Provider request failed");
1955 }
1956 }
1957 last_index = i;
1958 }
1959 }
1960
1961 if let Some(ref rf) = ravenfabric {
1963 if rf.is_enabled() {
1964 let _ = rf
1965 .broadcast("Single agent (multi-model) completed", 30)
1966 .await;
1967 info!("Multi-model result broadcast to RavenFabric mesh");
1968 }
1969 }
1970
1971 Ok(())
1972}
1973
1974pub async fn run_swarm_multi(
1979 multi_llm: MultiModelManager,
1980 config: Config,
1981 ravenfabric: Option<RavenFabricClient>,
1982) -> Result<()> {
1983 info!(
1984 "Starting swarm mode (multi-model) — {} parallel agents",
1985 multi_llm.client_count()
1986 );
1987
1988 if let Some(ref rf) = ravenfabric {
1990 if rf.is_enabled() {
1991 info!("RavenFabric remote execution available for swarm coordination");
1992 match rf.health().await {
1993 Ok(true) => info!("RavenFabric mesh is healthy"),
1994 Ok(false) => warn!("RavenFabric mesh returned unhealthy status"),
1995 Err(e) => warn!(error = %e, "RavenFabric health check failed"),
1996 }
1997 }
1998 }
1999
2000 let _system_prompt = &config.llm.system_prompt;
2001 let num_agents = multi_llm.client_count().min(3); let mut handles = Vec::new();
2003
2004 let personas = [
2006 "You are an analytical agent. Focus on logic, structure, and precision.",
2007 "You are a creative agent. Focus on innovation, alternatives, and possibilities.",
2008 "You are a pragmatic agent. Focus on simplicity, efficiency, and practicality.",
2009 ];
2010
2011 for i in 0..num_agents {
2012 let client = multi_llm.get_client(i).unwrap().clone();
2013 let persona = personas.get(i).unwrap_or(&personas[0]).to_string();
2014 let task = "Analyze the given task and provide your solution.".to_string();
2015
2016 let handle = tokio::spawn(async move {
2017 let mut memory = ConversationMemory::new(&persona, 10);
2018 memory.add_user_message(&task);
2019
2020 let messages = memory.history().to_vec();
2021 match client.chat(messages).await {
2022 Ok(response) => {
2023 let content = response
2024 .choices
2025 .first()
2026 .map(|c| c.message.content.clone())
2027 .unwrap_or_default();
2028 Ok((
2029 i,
2030 client.provider_name().to_string(),
2031 client.model().to_string(),
2032 content,
2033 ))
2034 }
2035 Err(e) => Err(format!("Agent {} failed: {}", i, e)),
2036 }
2037 });
2038
2039 handles.push(handle);
2040 }
2041
2042 let mut results: Vec<(usize, String, String, String)> = Vec::new();
2044 for handle in handles {
2045 match handle.await {
2046 Ok(Ok((idx, provider, model, result))) => {
2047 info!(
2048 "Agent {} ({}:{}) completed: {} chars",
2049 idx,
2050 provider,
2051 model,
2052 result.len()
2053 );
2054 results.push((idx, provider, model, result));
2055 }
2056 Ok(Err(e)) => warn!("Agent failed: {}", e),
2057 Err(e) => warn!("Agent join failed: {}", e),
2058 }
2059 }
2060
2061 println!(
2063 "\n🐦⬛ Swarm Results ({} agents, multi-model):",
2064 results.len()
2065 );
2066 for (idx, provider, model, result) in &results {
2067 println!("\n── Agent {} ({}:{}) ──", idx + 1, provider, model);
2068 println!("{}", result);
2069 }
2070
2071 if let Some(ref rf) = ravenfabric {
2073 if rf.is_enabled() {
2074 let summary = format!("Multi-model swarm completed: {} agents", results.len());
2075 let _ = rf.broadcast(&summary, 30).await;
2076 info!("Multi-model swarm results broadcast to RavenFabric mesh");
2077 }
2078 }
2079
2080 Ok(())
2081}
2082
2083pub async fn run_supervisor_multi(
2088 multi_llm: MultiModelManager,
2089 config: Config,
2090 ravenfabric: Option<RavenFabricClient>,
2091) -> Result<()> {
2092 info!(
2093 "Starting supervisor mode (multi-model) with {} providers",
2094 multi_llm.client_count()
2095 );
2096
2097 if let Some(ref rf) = ravenfabric {
2099 if rf.is_enabled() {
2100 info!("RavenFabric remote execution available for supervisor coordination");
2101 match rf.health().await {
2102 Ok(true) => info!("RavenFabric mesh is healthy"),
2103 Ok(false) => warn!("RavenFabric mesh returned unhealthy status"),
2104 Err(e) => warn!(error = %e, "RavenFabric health check failed"),
2105 }
2106 }
2107 }
2108
2109 let system_prompt = &config.llm.system_prompt;
2110 let policy_engine = PolicyEngine::default_secure();
2111 let mut sandbox = Sandbox::default();
2112 sandbox.init().await.map_err(|e| {
2113 crate::error::RavenClawsError::CommandExecution(format!("Sandbox init failed: {}", e))
2114 })?;
2115 let audit_log = AuditLog::new(format!("supervisor-multi-{}", std::process::id()));
2116 let registry = ToolRegistry::with_default_tools();
2117
2118 let supervisor_prompt = format!(
2120 "You are a supervisor agent coordinating multiple LLM providers. \
2121 Decompose tasks and assign them to appropriate providers based on their strengths. \
2122 \n\nFor each subtask, respond with:\n\
2123 SUBTASK: <description>\n\
2124 PROVIDER: <provider_index 0-{}>\n\
2125 \nWhen complete, respond with:\n\
2126 FINAL: <aggregated result>\n\
2127 \nTask: {}",
2128 multi_llm.client_count() - 1,
2129 "Coordinate the completion of the assigned task using available providers."
2130 );
2131
2132 let mut memory = ConversationMemory::new(system_prompt, 20);
2133 memory.add_user_message(&supervisor_prompt);
2134
2135 let mut subtask_results: Vec<String> = Vec::new();
2136 let mut iteration = 0;
2137 let max_iterations = 15;
2138
2139 loop {
2140 iteration += 1;
2141 if iteration > max_iterations {
2142 warn!("Supervisor reached max iterations");
2143 break;
2144 }
2145
2146 let supervisor_client = multi_llm
2148 .get_client(iteration % multi_llm.client_count())
2149 .or_else(|| multi_llm.get_client(0))
2150 .cloned();
2151
2152 let messages = memory.history().to_vec();
2153 let response =
2154 match supervisor_client.map(|c| tokio::spawn(async move { c.chat(messages).await })) {
2155 Some(handle) => match handle.await {
2156 Ok(Ok(r)) => r,
2157 Ok(Err(e)) => {
2158 warn!(error = %e, "Supervisor LLM request failed");
2159 continue;
2160 }
2161 Err(e) => {
2162 warn!(error = %e, "Supervisor task join failed");
2163 continue;
2164 }
2165 },
2166 None => {
2167 warn!("No LLM clients available");
2168 break;
2169 }
2170 };
2171
2172 let content = response
2173 .choices
2174 .first()
2175 .map(|c| c.message.content.clone())
2176 .unwrap_or_default();
2177
2178 if content.contains("FINAL:") {
2180 let final_response = content
2181 .split("FINAL:")
2182 .nth(1)
2183 .unwrap_or("")
2184 .trim()
2185 .to_string();
2186 info!("Supervisor completed task: {} chars", final_response.len());
2187
2188 let _ = audit_log.append(
2189 AuditEventType::AgentFinish,
2190 "supervisor",
2191 "Supervisor completed task coordination",
2192 Some(serde_json::json!({
2193 "iterations": iteration,
2194 "subtasks_completed": subtask_results.len(),
2195 "providers_used": multi_llm.client_count(),
2196 })),
2197 );
2198
2199 println!("\n🐦⬛ Supervisor Result (multi-model):\n{}", final_response);
2200 return Ok(());
2201 }
2202
2203 if content.contains("SUBTASK:") && content.contains("PROVIDER:") {
2205 let subtask_block = content.split("SUBTASK:").nth(1).unwrap_or("");
2206 let subtask_lines: Vec<&str> = subtask_block.lines().take(4).collect();
2207
2208 let subtask_desc = subtask_lines.first().unwrap_or(&"").trim();
2209 let provider_idx = subtask_lines
2210 .iter()
2211 .find(|l| l.starts_with("PROVIDER:"))
2212 .and_then(|l| l.split(':').nth(1))
2213 .and_then(|s| s.trim().parse::<usize>().ok())
2214 .unwrap_or(0);
2215
2216 if !subtask_desc.is_empty() {
2217 info!("Subtask for provider {}: {}", provider_idx, subtask_desc);
2218
2219 let client = multi_llm
2220 .get_client(provider_idx)
2221 .or_else(|| multi_llm.get_client(0));
2222
2223 if let Some(client) = client {
2224 let subtask_result = run_subtask_agent(
2225 client.clone(),
2226 subtask_desc,
2227 system_prompt,
2228 &policy_engine,
2229 &sandbox,
2230 &audit_log,
2231 ®istry,
2232 )
2233 .await;
2234
2235 match subtask_result {
2236 Ok(result) => {
2237 info!("Subtask {} completed: {} chars", provider_idx, result.len());
2238 subtask_results.push(format!(
2239 "Provider {} ({}): {}",
2240 provider_idx,
2241 client.provider_name(),
2242 result
2243 ));
2244
2245 memory.add_assistant_message(&format!(
2246 "Assigned subtask to provider {}: {}",
2247 provider_idx, subtask_desc
2248 ));
2249 memory.add_user_message(&format!(
2250 "Provider {} result: {}",
2251 provider_idx, result
2252 ));
2253 }
2254 Err(e) => {
2255 warn!("Subtask {} failed: {}", provider_idx, e);
2256 memory.add_assistant_message(&format!(
2257 "Provider {} subtask failed: {}",
2258 provider_idx, e
2259 ));
2260 }
2261 }
2262 }
2263 }
2264 } else {
2265 memory.add_assistant_message(&content);
2266 }
2267 }
2268
2269 if !subtask_results.is_empty() {
2271 let aggregated = subtask_results.join("\n\n");
2272 info!(
2273 "Supervisor aggregated {} subtask results",
2274 subtask_results.len()
2275 );
2276
2277 if let Some(ref rf) = ravenfabric {
2279 if rf.is_enabled() {
2280 let summary = format!(
2281 "Multi-model supervisor completed: {} subtasks, result: {} chars",
2282 subtask_results.len(),
2283 aggregated.len()
2284 );
2285 let _ = rf.broadcast(&summary, 30).await;
2286 info!("Multi-model supervisor result broadcast to RavenFabric mesh");
2287 }
2288 }
2289
2290 println!(
2291 "\n🐦⬛ Supervisor Aggregated Result (multi-model):\n{}",
2292 aggregated
2293 );
2294 return Ok(());
2295 }
2296
2297 Err(crate::error::RavenClawsError::CommandExecution(
2298 "Supervisor mode completed without results".to_string(),
2299 ))
2300}
2301
2302pub async fn run_repl(llm: Arc<dyn LLMProviderTrait>, config: Config) -> Result<()> {
2304 use tokio::io::{AsyncBufReadExt, BufReader};
2305
2306 info!("Starting interactive REPL mode");
2307
2308 let system_prompt = &config.llm.system_prompt;
2309 let mut memory = ConversationMemory::new(system_prompt, 0);
2310
2311 let stdin = BufReader::new(tokio::io::stdin());
2312 let mut lines = stdin.lines();
2313
2314 println!("RavenClaws REPL — type /exit to quit, /reset to clear history");
2315
2316 loop {
2317 print!("\n> ");
2318 use tokio::io::AsyncWriteExt;
2319 tokio::io::stdout().flush().await?;
2320
2321 let line = match lines.next_line().await {
2322 Ok(Some(l)) => l,
2323 Ok(None) => break, Err(e) => {
2325 warn!(error = %e, "REPL read error");
2326 break;
2327 }
2328 };
2329
2330 let input = line.trim();
2331
2332 if input.is_empty() {
2333 continue;
2334 }
2335
2336 match input {
2337 "/exit" | "/quit" => {
2338 println!("Exiting REPL.");
2339 break;
2340 }
2341 "/reset" => {
2342 memory = ConversationMemory::new(system_prompt, 0);
2343 println!("Conversation history reset.");
2344 continue;
2345 }
2346 _ => {}
2347 }
2348
2349 memory.add_user_message(input);
2350 let messages = memory.history().to_vec();
2351
2352 match llm.chat(messages).await {
2353 Ok(response) => {
2354 if let Some(choice) = response.choices.first() {
2355 let content = &choice.message.content;
2356 println!("{}", content);
2357 memory.add_assistant_message(content);
2358 }
2359 }
2360 Err(e) => {
2361 warn!(error = %e, "LLM request failed");
2362 println!("Error: {}", e);
2363 }
2364 }
2365 }
2366
2367 Ok(())
2368}
2369
2370#[cfg(test)]
2371mod tests {
2372 use super::*;
2373
2374 #[test]
2375 fn test_swarm_function_exists() {
2376 let _fn_ptr: fn(Arc<dyn LLMProviderTrait>, Config, Option<RavenFabricClient>) -> _ =
2378 run_swarm;
2379 }
2380
2381 #[test]
2382 fn test_supervisor_function_exists() {
2383 let _fn_ptr: fn(Arc<dyn LLMProviderTrait>, Config, Option<RavenFabricClient>) -> _ =
2385 run_supervisor;
2386 }
2387
2388 #[test]
2389 fn test_conversation_memory_new() {
2390 let mem = ConversationMemory::new("system prompt", 10);
2391 assert_eq!(mem.messages.len(), 1);
2392 assert_eq!(mem.messages[0].role, "system");
2393 assert_eq!(mem.messages[0].content, "system prompt");
2394 }
2395
2396 #[test]
2397 fn test_conversation_memory_add_user() {
2398 let mut mem = ConversationMemory::new("system", 10);
2399 mem.add_user_message("hello");
2400 assert_eq!(mem.messages.len(), 2);
2401 assert_eq!(mem.messages[1].role, "user");
2402 assert_eq!(mem.messages[1].content, "hello");
2403 }
2404
2405 #[test]
2406 fn test_conversation_memory_trim() {
2407 let mut mem = ConversationMemory::new("system", 3);
2408 mem.add_user_message("msg1");
2409 mem.add_assistant_message("resp1");
2410 mem.add_user_message("msg2");
2411 mem.add_assistant_message("resp2");
2412 assert!(mem.messages.len() <= 3);
2414 }
2415
2416 #[test]
2417 fn test_parse_tool_call_valid() {
2418 let content = "THOUGHT: I need to run a command\nTOOL_CALL: shell_exec\nARGS: {\"command\": \"echo hello\"}";
2419 let (name, args) = parse_tool_call(content).unwrap();
2420 assert_eq!(name, "shell_exec");
2421 assert_eq!(args["command"], "echo hello");
2422 }
2423
2424 #[test]
2425 fn test_parse_tool_call_missing_tool() {
2426 let content = "THOUGHT: no tool here";
2427 assert!(parse_tool_call(content).is_none());
2428 }
2429
2430 #[test]
2431 fn test_parse_tool_call_missing_args() {
2432 let content = "TOOL_CALL: shell_exec\nNo args line";
2433 assert!(parse_tool_call(content).is_none());
2434 }
2435
2436 #[test]
2437 fn test_parse_tool_call_invalid_json() {
2438 let content = "TOOL_CALL: shell_exec\nARGS: not valid json";
2439 assert!(parse_tool_call(content).is_none());
2440 }
2441
2442 #[test]
2443 fn test_agent_loop_config_default() {
2444 let config = AgentLoopConfig::default();
2445 assert_eq!(config.max_iterations, 10);
2446 assert!(!config.enable_tools);
2447 assert!(!config.require_approval);
2448 }
2449
2450 #[test]
2451 fn test_agent_loop_config_require_approval() {
2452 let config = AgentLoopConfig {
2453 max_iterations: 5,
2454 enable_tools: true,
2455 require_approval: true,
2456 prompt_injection_protection: true,
2457 token_lifetime_secs: 0,
2458 no_final_required: false,
2459 fallback_chain: None,
2460 token_budget: None,
2461 ravenfabric: None,
2462 checkpoint_dir: None,
2463 session_id: None,
2464 metrics_callback: None,
2465 load_manager: None,
2466 retry_config: None,
2467 };
2468 assert_eq!(config.max_iterations, 5);
2469 assert!(config.enable_tools);
2470 assert!(config.require_approval);
2471 assert!(config.prompt_injection_protection);
2472 assert_eq!(config.token_lifetime_secs, 0);
2473 }
2474
2475 #[test]
2476 fn test_prompt_for_approval_yes() {
2477 let args = serde_json::json!({"command": "echo hello"});
2478 let result = tokio_test::block_on(prompt_for_approval_with_input("shell_exec", &args, "y"));
2479 assert!(result, "Should approve for 'y'");
2480 }
2481
2482 #[test]
2483 fn test_prompt_for_approval_yes_full() {
2484 let args = serde_json::json!({"command": "echo hello"});
2485 let result =
2486 tokio_test::block_on(prompt_for_approval_with_input("shell_exec", &args, "yes"));
2487 assert!(result, "Should approve for 'yes'");
2488 }
2489
2490 #[test]
2491 fn test_prompt_for_approval_no() {
2492 let args = serde_json::json!({"command": "echo hello"});
2493 let result = tokio_test::block_on(prompt_for_approval_with_input("shell_exec", &args, "n"));
2494 assert!(!result, "Should deny for 'n'");
2495 }
2496
2497 #[test]
2498 fn test_prompt_for_approval_no_full() {
2499 let args = serde_json::json!({"command": "echo hello"});
2500 let result =
2501 tokio_test::block_on(prompt_for_approval_with_input("shell_exec", &args, "no"));
2502 assert!(!result, "Should deny for 'no'");
2503 }
2504
2505 #[test]
2506 fn test_prompt_for_approval_empty() {
2507 let args = serde_json::json!({"command": "echo hello"});
2508 let result = tokio_test::block_on(prompt_for_approval_with_input("shell_exec", &args, ""));
2509 assert!(!result, "Should deny for empty input (default N)");
2510 }
2511
2512 #[test]
2513 fn test_prompt_for_approval_uppercase() {
2514 let args = serde_json::json!({"command": "echo hello"});
2515 let result = tokio_test::block_on(prompt_for_approval_with_input("shell_exec", &args, "Y"));
2516 assert!(result, "Should approve for uppercase 'Y'");
2517 }
2518
2519 #[test]
2520 fn test_prompt_for_approval_auto_approves_non_tty() {
2521 #[allow(clippy::let_underscore_future)]
2527 let _ = prompt_for_approval_with_input("test", &serde_json::json!({}), "y");
2528 }
2529
2530 #[test]
2531 fn test_execute_parsed_tool_call_skips_approval_when_not_required() {
2532 let registry = ToolRegistry::with_default_tools();
2533 let policy_engine = PolicyEngine::default_secure();
2534 let sandbox = Sandbox::default();
2535 let audit_log = AuditLog::new("test-session".to_string());
2536
2537 let args = serde_json::json!({"command": "echo hello"});
2538 let result = tokio_test::block_on(execute_parsed_tool_call(
2539 "shell_exec".to_string(),
2540 args,
2541 ®istry,
2542 &policy_engine,
2543 &sandbox,
2544 &audit_log,
2545 false, ));
2547
2548 assert!(result.is_some());
2549 let tool_result = result.unwrap();
2550 assert_eq!(tool_result.tool_name, "shell_exec");
2551 }
2552
2553 #[test]
2554 fn test_execute_parsed_tool_call_approval_not_needed_for_read_only_tools() {
2555 let registry = ToolRegistry::with_default_tools();
2558 let policy_engine = PolicyEngine::default_secure();
2559 let sandbox = Sandbox::default();
2560 let audit_log = AuditLog::new("test-session".to_string());
2561
2562 let args = serde_json::json!({"path": "/tmp/test.txt"});
2563 let result = tokio_test::block_on(execute_parsed_tool_call(
2564 "read_file".to_string(),
2565 args,
2566 ®istry,
2567 &policy_engine,
2568 &sandbox,
2569 &audit_log,
2570 true, ));
2572
2573 assert!(result.is_some());
2575 let tool_result = result.unwrap();
2576 assert_eq!(tool_result.tool_name, "read_file");
2577 }
2578
2579 #[test]
2580 fn test_agent_loop_config_token_lifetime_zero_disabled() {
2581 let config = AgentLoopConfig {
2582 max_iterations: 10,
2583 enable_tools: false,
2584 require_approval: false,
2585 prompt_injection_protection: false,
2586 token_lifetime_secs: 0,
2587 no_final_required: false,
2588 fallback_chain: None,
2589 token_budget: None,
2590 ravenfabric: None,
2591 checkpoint_dir: None,
2592 session_id: None,
2593 metrics_callback: None,
2594 load_manager: None,
2595 retry_config: None,
2596 };
2597 assert_eq!(config.token_lifetime_secs, 0);
2598 }
2600
2601 #[test]
2602 fn test_agent_loop_config_token_lifetime_nonzero() {
2603 let config = AgentLoopConfig {
2604 max_iterations: 10,
2605 enable_tools: false,
2606 require_approval: false,
2607 prompt_injection_protection: false,
2608 token_lifetime_secs: 3600,
2609 no_final_required: false,
2610 fallback_chain: None,
2611 token_budget: None,
2612 ravenfabric: None,
2613 checkpoint_dir: None,
2614 session_id: None,
2615 metrics_callback: None,
2616 load_manager: None,
2617 retry_config: None,
2618 };
2619 assert_eq!(config.token_lifetime_secs, 3600);
2620 }
2621
2622 #[test]
2623 fn test_agent_loop_config_default_includes_token_lifetime() {
2624 let config = AgentLoopConfig::default();
2625 assert_eq!(config.token_lifetime_secs, 0);
2626 }
2627
2628 #[test]
2629 fn test_agent_loop_config_retry_config_default_none() {
2630 let config = AgentLoopConfig::default();
2631 assert!(config.retry_config.is_none());
2632 }
2633
2634 #[test]
2635 fn test_agent_loop_config_retry_config_custom() {
2636 let config = AgentLoopConfig {
2637 max_iterations: 10,
2638 enable_tools: false,
2639 require_approval: false,
2640 prompt_injection_protection: false,
2641 token_lifetime_secs: 0,
2642 no_final_required: false,
2643 fallback_chain: None,
2644 token_budget: None,
2645 ravenfabric: None,
2646 checkpoint_dir: None,
2647 session_id: None,
2648 metrics_callback: None,
2649 load_manager: None,
2650 retry_config: Some(RetryConfig {
2651 max_retries: 5,
2652 base_delay_ms: 50,
2653 max_delay_ms: 5000,
2654 jitter: 0.1,
2655 }),
2656 };
2657 assert!(config.retry_config.is_some());
2658 assert_eq!(config.retry_config.as_ref().unwrap().max_retries, 5);
2659 assert_eq!(config.retry_config.as_ref().unwrap().base_delay_ms, 50);
2660 }
2661
2662 #[test]
2663 fn test_retry_config_delay_calculation() {
2664 let config = RetryConfig {
2665 max_retries: 3,
2666 base_delay_ms: 100,
2667 max_delay_ms: 10000,
2668 jitter: 0.0, };
2670
2671 let d0 = config.delay_for_attempt(0);
2673 assert_eq!(d0.as_millis(), 100);
2674
2675 let d1 = config.delay_for_attempt(1);
2677 assert_eq!(d1.as_millis(), 200);
2678
2679 let d2 = config.delay_for_attempt(2);
2681 assert_eq!(d2.as_millis(), 400);
2682 }
2683
2684 #[test]
2685 fn test_retry_config_delay_capped() {
2686 let config = RetryConfig {
2687 max_retries: 10,
2688 base_delay_ms: 1000,
2689 max_delay_ms: 5000,
2690 jitter: 0.0, };
2692
2693 let d5 = config.delay_for_attempt(5);
2695 assert_eq!(d5.as_millis(), 5000);
2696 }
2697
2698 #[test]
2699 fn test_retry_config_delay_with_jitter() {
2700 let config = RetryConfig {
2701 max_retries: 3,
2702 base_delay_ms: 100,
2703 max_delay_ms: 10000,
2704 jitter: 0.5,
2705 };
2706
2707 let d = config.delay_for_attempt(0);
2709 assert!(d.as_millis() >= 100);
2710 assert!(d.as_millis() <= 150);
2712 }
2713}