1use crate::audit::{AuditEventType, AuditLog};
7use crate::config::Config;
8use crate::error::Result;
9use crate::llm::{
10 ChatMessage, Choice, LLMProviderTrait, MultiModelManager, ProviderFallbackChain, TokenBudget,
11};
12use crate::mcp::McpClient;
13use crate::policy::{Decision, PolicyEngine};
14use crate::ravenfabric::RavenFabricClient;
15use crate::sandbox::Sandbox;
16use crate::tools::{ToolCall, ToolRegistry, ToolResult};
17use std::sync::Arc;
18use tokio::sync::RwLock;
19use tracing::{debug, info, instrument, warn};
20
21#[derive(Debug, Clone)]
24pub struct ConversationMemory {
25 max_messages: usize,
27 messages: Vec<ChatMessage>,
29}
30
31impl ConversationMemory {
32 pub fn new(system_prompt: &str, max_messages: usize) -> Self {
35 Self {
36 max_messages,
37 messages: vec![ChatMessage {
38 role: "system".to_string(),
39 content: system_prompt.to_string(),
40 }],
41 }
42 }
43
44 pub fn add_user_message(&mut self, content: &str) -> &[ChatMessage] {
46 self.messages.push(ChatMessage {
47 role: "user".to_string(),
48 content: content.to_string(),
49 });
50 self.trim_to_max();
51 &self.messages
52 }
53
54 pub fn add_assistant_message(&mut self, content: &str) {
56 self.messages.push(ChatMessage {
57 role: "assistant".to_string(),
58 content: content.to_string(),
59 });
60 self.trim_to_max();
61 }
62
63 pub fn history(&self) -> &[ChatMessage] {
65 &self.messages
66 }
67
68 fn trim_to_max(&mut self) {
70 if self.max_messages == 0 {
71 return;
72 }
73 while self.messages.len() > self.max_messages {
74 if self.messages.len() > 1 {
76 self.messages.remove(1);
77 } else {
78 break;
79 }
80 }
81 }
82}
83
84#[derive(Debug, Clone)]
86pub struct AgentLoopConfig {
87 pub max_iterations: usize,
89 pub enable_tools: bool,
91 pub require_approval: bool,
93 pub prompt_injection_protection: bool,
95 pub token_lifetime_secs: u64,
99 pub no_final_required: bool,
101 pub fallback_chain: Option<Arc<std::sync::Mutex<ProviderFallbackChain>>>,
103 pub token_budget: Option<Arc<std::sync::Mutex<TokenBudget>>>,
105 pub ravenfabric: Option<RavenFabricClient>,
107}
108
109impl Default for AgentLoopConfig {
110 fn default() -> Self {
111 Self {
112 max_iterations: 10,
113 enable_tools: false,
114 require_approval: false,
115 prompt_injection_protection: true,
116 token_lifetime_secs: 0,
117 no_final_required: false,
118 fallback_chain: None,
119 token_budget: None,
120 ravenfabric: None,
121 }
122 }
123}
124
125#[instrument(skip_all, fields(provider = %llm.provider_name(), model = %llm.model()))]
132pub async fn run_agent_loop(
133 llm: Arc<dyn LLMProviderTrait>,
134 initial_prompt: &str,
135 system_prompt: &str,
136 config: AgentLoopConfig,
137) -> Result<String> {
138 run_agent_loop_with_registry(llm, initial_prompt, system_prompt, config, None).await
139}
140
141#[instrument(skip_all, fields(provider = %llm.provider_name(), model = %llm.model()))]
146pub async fn run_agent_loop_with_registry(
147 llm: Arc<dyn LLMProviderTrait>,
148 initial_prompt: &str,
149 system_prompt: &str,
150 config: AgentLoopConfig,
151 tool_registry: Option<ToolRegistry>,
152) -> Result<String> {
153 let policy_engine = PolicyEngine::default_secure();
155 let mut sandbox = Sandbox::default();
156 sandbox.init().await.map_err(|e| {
157 crate::error::RavenClawsError::CommandExecution(format!("Sandbox init failed: {}", e))
158 })?;
159 let audit_log = AuditLog::new(format!("agent-{}", std::process::id()));
160
161 let injection_detector = if config.prompt_injection_protection {
163 Some(crate::policy::InjectionDetector::new())
164 } else {
165 None
166 };
167
168 let registry = tool_registry.unwrap_or_else(ToolRegistry::with_default_tools);
170
171 let session_start = std::time::Instant::now();
173
174 info!(
175 provider = llm.provider_name(),
176 model = llm.model(),
177 max_iterations = config.max_iterations,
178 enable_tools = config.enable_tools,
179 require_approval = config.require_approval,
180 prompt_injection_protection = config.prompt_injection_protection,
181 token_lifetime_secs = config.token_lifetime_secs,
182 "Agent loop starting with security integration"
183 );
184
185 let _ = audit_log.append(
187 AuditEventType::AgentStart,
188 "agent",
189 &format!(
190 "Agent loop started with {} (model: {})",
191 llm.provider_name(),
192 llm.model()
193 ),
194 Some(serde_json::json!({
195 "provider": llm.provider_name(),
196 "model": llm.model(),
197 "max_iterations": config.max_iterations,
198 "enable_tools": config.enable_tools,
199 "require_approval": config.require_approval,
200 "prompt_injection_protection": config.prompt_injection_protection,
201 "token_lifetime_secs": config.token_lifetime_secs,
202 })),
203 );
204
205 let mut memory = ConversationMemory::new(system_prompt, 0);
206 memory.add_user_message(initial_prompt);
207
208 for iteration in 0..config.max_iterations {
209 if config.token_lifetime_secs > 0 {
211 let elapsed = session_start.elapsed().as_secs();
212 if elapsed >= config.token_lifetime_secs {
213 warn!(
214 iteration = iteration,
215 elapsed_secs = elapsed,
216 token_lifetime_secs = config.token_lifetime_secs,
217 "Agent loop reached token lifetime limit"
218 );
219 let _ = audit_log.append(
220 AuditEventType::SecurityViolation,
221 "token_lifetime",
222 &format!(
223 "Session expired after {} seconds (limit: {}s)",
224 elapsed, config.token_lifetime_secs
225 ),
226 Some(serde_json::json!({
227 "elapsed_secs": elapsed,
228 "token_lifetime_secs": config.token_lifetime_secs,
229 "iteration": iteration,
230 })),
231 );
232 return Err(crate::error::RavenClawsError::SecurityViolation(format!(
233 "Session token expired after {} seconds (limit: {}s)",
234 elapsed, config.token_lifetime_secs
235 )));
236 }
237 }
238 let messages = memory.history().to_vec();
239
240 if let Some(ref budget) = config.token_budget {
242 let budget = budget.lock().unwrap();
243 if budget.remaining() < 100 {
244 warn!(
245 iteration = iteration,
246 remaining = budget.remaining(),
247 "Token budget exhausted"
248 );
249 let _ = audit_log.append(
250 AuditEventType::SecurityViolation,
251 "token_budget",
252 &format!("Token budget exhausted (remaining: {})", budget.remaining()),
253 Some(serde_json::json!({
254 "remaining": budget.remaining(),
255 "used": budget.used_tokens,
256 "iteration": iteration,
257 })),
258 );
259 return Err(crate::error::RavenClawsError::SecurityViolation(
260 "Token budget exhausted".to_string(),
261 ));
262 }
263 }
264
265 let response = match llm.chat(messages.clone()).await {
266 Ok(r) => r,
267 Err(e) => {
268 if let Some(ref chain) = config.fallback_chain {
270 warn!(error = %e, "Primary LLM failed, trying fallback chain");
271 let _ = audit_log.append(
272 AuditEventType::Error,
273 "llm",
274 &format!("Primary LLM failed, trying fallback: {}", e),
275 None,
276 );
277 let configs = {
279 let c = chain.lock().unwrap();
280 c.configs.clone()
281 };
282 let mut temp_chain = ProviderFallbackChain::new(configs);
283 match temp_chain.chat_with_fallback(messages).await {
284 Ok(r) => {
285 info!("Fallback chain succeeded");
286 if let Some(ref budget) = config.token_budget {
288 if let Some(usage) = &r.usage {
289 let mut b = budget.lock().unwrap();
290 b.record_usage(usage.total_tokens);
291 }
292 }
293 r
294 }
295 Err(fallback_e) => {
296 warn!(error = %fallback_e, "Fallback chain also failed");
297 let _ = audit_log.append(
298 AuditEventType::Error,
299 "llm",
300 &format!("All providers failed: {}", fallback_e),
301 None,
302 );
303 return Err(crate::error::RavenClawsError::Llm(fallback_e));
304 }
305 }
306 } else {
307 warn!(error = %e, "LLM request failed");
308 let _ = audit_log.append(
309 AuditEventType::Error,
310 "llm",
311 &format!("LLM request failed: {}", e),
312 None,
313 );
314 return Err(crate::error::RavenClawsError::Llm(e));
315 }
316 }
317 };
318
319 if let Some(ref budget) = config.token_budget {
321 if let Some(usage) = &response.usage {
322 let mut b = budget.lock().unwrap();
323 b.record_usage(usage.total_tokens);
324 debug!(
325 iteration = iteration,
326 tokens_used = usage.total_tokens,
327 total_used = b.used_tokens,
328 remaining = b.remaining(),
329 "Token usage recorded"
330 );
331 }
332 }
333
334 if let Some(ref rf) = config.ravenfabric {
336 if rf.is_enabled() {
337 let _ = rf.health().await;
338 info!(
339 iteration = iteration,
340 ravenfabric = true,
341 "RavenFabric health check completed"
342 );
343 }
344 }
345
346 let first_choice = response.choices.first();
347 let content = first_choice
348 .map(|c| c.message.content.clone())
349 .unwrap_or_default();
350
351 debug!(
352 iteration = iteration,
353 response_length = content.len(),
354 response_preview = %content[..content.len().min(500)],
355 "LLM response received"
356 );
357
358 if let Some(ref detector) = injection_detector {
360 match detector.check(&content) {
361 crate::policy::InjectionVerdict::Suspicious(reason) => {
362 warn!(
363 iteration = iteration,
364 reason = %reason,
365 "Prompt-injection detected in LLM response"
366 );
367 let _ = audit_log.append(
368 AuditEventType::SecurityViolation,
369 "injection_detector",
370 &format!("Prompt-injection detected: {}", reason),
371 Some(serde_json::json!({
372 "reason": reason,
373 "iteration": iteration,
374 "content_preview": &content[..content.len().min(200)],
375 })),
376 );
377 return Err(crate::error::RavenClawsError::SecurityViolation(format!(
378 "LLM response blocked: potential prompt injection ({})",
379 reason
380 )));
381 }
382 crate::policy::InjectionVerdict::Clean => {}
383 }
384 }
385
386 if config.enable_tools {
388 if let Some((tool_name, args)) = first_choice.and_then(parse_structured_tool_call) {
389 info!(tool = %tool_name, "Structured tool call detected");
390
391 if let Some(tool_result) = execute_parsed_tool_call(
393 tool_name,
394 args,
395 ®istry,
396 &policy_engine,
397 &sandbox,
398 &audit_log,
399 config.require_approval,
400 )
401 .await
402 {
403 let observation = if tool_result.success {
404 format!("OBSERVATION: {}", tool_result.output)
405 } else {
406 format!(
407 "OBSERVATION: Tool failed with error: {}",
408 tool_result.error.as_deref().unwrap_or("unknown error")
409 )
410 };
411
412 memory.add_user_message(&observation);
413
414 info!(
415 iteration = iteration,
416 tool = %tool_result.tool_name,
417 success = tool_result.success,
418 "Structured tool executed"
419 );
420 continue;
421 }
422 }
423 }
424
425 if content.contains("FINAL:") {
427 let final_response = content
428 .split("FINAL:")
429 .nth(1)
430 .unwrap_or("")
431 .trim()
432 .to_string();
433
434 memory.add_assistant_message(&content);
435
436 let _ = audit_log.append(
438 AuditEventType::AgentFinish,
439 "agent",
440 "Agent loop completed successfully",
441 Some(serde_json::json!({
442 "iterations": iteration + 1,
443 "final_response_length": final_response.len(),
444 })),
445 );
446
447 return Ok(final_response);
448 }
449
450 if config.enable_tools {
452 if let Some(tool_result) = execute_tool_call_with_security(
453 &content,
454 ®istry,
455 &policy_engine,
456 &sandbox,
457 &audit_log,
458 )
459 .await
460 {
461 let observation = if tool_result.success {
462 format!("OBSERVATION: {}", tool_result.output)
463 } else {
464 format!(
465 "OBSERVATION: Tool failed with error: {}",
466 tool_result.error.as_deref().unwrap_or("unknown error")
467 )
468 };
469
470 memory.add_assistant_message(&content);
471 memory.add_user_message(&observation);
472
473 info!(
474 iteration = iteration,
475 tool = %tool_result.tool_name,
476 success = tool_result.success,
477 "Tool executed"
478 );
479 continue;
480 }
481 }
482
483 memory.add_assistant_message(&content);
485
486 if config.no_final_required {
488 info!(
489 iteration = iteration,
490 response_length = content.len(),
491 "no_final_required: treating response as completion"
492 );
493 let _ = audit_log.append(
494 AuditEventType::AgentFinish,
495 "agent",
496 "Agent loop completed (no_final_required)",
497 Some(serde_json::json!({
498 "iterations": iteration + 1,
499 "final_response_length": content.len(),
500 })),
501 );
502 return Ok(content);
503 }
504
505 info!(
506 iteration = iteration,
507 thought = %content.lines().find(|l| l.starts_with("THOUGHT:")).unwrap_or("<no thought>"),
508 "Agent loop progress"
509 );
510 }
511
512 warn!(
514 max_iterations = config.max_iterations,
515 "Agent loop reached max iterations"
516 );
517
518 let _ = audit_log.append(
519 AuditEventType::Error,
520 "agent",
521 "Agent loop reached max iterations without completing",
522 Some(serde_json::json!({
523 "max_iterations": config.max_iterations,
524 })),
525 );
526
527 let history = memory.history();
528 if history.len() > 1 {
529 if let Some(last) = history.last() {
530 return Ok(last.content.clone());
531 }
532 }
533
534 Err(crate::error::RavenClawsError::CommandExecution(
535 "Agent loop reached max iterations without completing the task".to_string(),
536 ))
537}
538
539#[allow(dead_code)]
545#[instrument(skip_all, fields(provider = %llm.provider_name(), model = %llm.model()))]
546pub async fn run_agent_loop_with_mcp(
547 llm: Arc<dyn LLMProviderTrait>,
548 initial_prompt: &str,
549 system_prompt: &str,
550 config: AgentLoopConfig,
551 mcp_client: Option<Arc<RwLock<McpClient>>>,
552) -> Result<String> {
553 run_agent_loop_with_mcp_and_registry(
554 llm,
555 initial_prompt,
556 system_prompt,
557 config,
558 mcp_client,
559 None,
560 )
561 .await
562}
563
564#[instrument(skip_all, fields(provider = %llm.provider_name(), model = %llm.model()))]
566pub async fn run_agent_loop_with_mcp_and_registry(
567 llm: Arc<dyn LLMProviderTrait>,
568 initial_prompt: &str,
569 system_prompt: &str,
570 config: AgentLoopConfig,
571 mcp_client: Option<Arc<RwLock<McpClient>>>,
572 tool_registry: Option<ToolRegistry>,
573) -> Result<String> {
574 let policy_engine = PolicyEngine::default_secure();
576 let mut sandbox = Sandbox::default();
577 sandbox.init().await.map_err(|e| {
578 crate::error::RavenClawsError::CommandExecution(format!("Sandbox init failed: {}", e))
579 })?;
580 let audit_log = AuditLog::new(format!("agent-{}", std::process::id()));
581
582 let injection_detector = if config.prompt_injection_protection {
584 Some(crate::policy::InjectionDetector::new())
585 } else {
586 None
587 };
588
589 let mut registry = tool_registry.unwrap_or_else(ToolRegistry::with_default_tools);
591
592 if let Some(client) = &mcp_client {
594 match crate::mcp::register_mcp_tools(&mut registry, client.clone()).await {
595 Ok(count) => {
596 info!(count, "MCP tools registered");
597 }
598 Err(e) => {
599 warn!(error = %e, "Failed to register MCP tools");
600 }
601 }
602 }
603
604 let session_start = std::time::Instant::now();
606
607 info!(
608 provider = llm.provider_name(),
609 model = llm.model(),
610 max_iterations = config.max_iterations,
611 enable_tools = config.enable_tools,
612 tool_count = registry.len(),
613 require_approval = config.require_approval,
614 prompt_injection_protection = config.prompt_injection_protection,
615 token_lifetime_secs = config.token_lifetime_secs,
616 "Agent loop starting with MCP integration"
617 );
618
619 let _ = audit_log.append(
621 AuditEventType::AgentStart,
622 "agent",
623 &format!(
624 "Agent loop started with {} (model: {})",
625 llm.provider_name(),
626 llm.model()
627 ),
628 Some(serde_json::json!({
629 "provider": llm.provider_name(),
630 "model": llm.model(),
631 "max_iterations": config.max_iterations,
632 "enable_tools": config.enable_tools,
633 "mcp_enabled": mcp_client.is_some(),
634 "tool_count": registry.len(),
635 "require_approval": config.require_approval,
636 "prompt_injection_protection": config.prompt_injection_protection,
637 "token_lifetime_secs": config.token_lifetime_secs,
638 })),
639 );
640
641 let mut memory = ConversationMemory::new(system_prompt, 0);
642 memory.add_user_message(initial_prompt);
643
644 for iteration in 0..config.max_iterations {
645 if config.token_lifetime_secs > 0 {
647 let elapsed = session_start.elapsed().as_secs();
648 if elapsed >= config.token_lifetime_secs {
649 warn!(
650 iteration = iteration,
651 elapsed_secs = elapsed,
652 token_lifetime_secs = config.token_lifetime_secs,
653 "Agent loop reached token lifetime limit"
654 );
655 let _ = audit_log.append(
656 AuditEventType::SecurityViolation,
657 "token_lifetime",
658 &format!(
659 "Session expired after {} seconds (limit: {}s)",
660 elapsed, config.token_lifetime_secs
661 ),
662 Some(serde_json::json!({
663 "elapsed_secs": elapsed,
664 "token_lifetime_secs": config.token_lifetime_secs,
665 "iteration": iteration,
666 })),
667 );
668 return Err(crate::error::RavenClawsError::SecurityViolation(format!(
669 "Session token expired after {} seconds (limit: {}s)",
670 elapsed, config.token_lifetime_secs
671 )));
672 }
673 }
674 let messages = memory.history().to_vec();
675
676 if let Some(ref budget) = config.token_budget {
678 let budget = budget.lock().unwrap();
679 if budget.remaining() < 100 {
680 warn!(
681 iteration = iteration,
682 remaining = budget.remaining(),
683 "Token budget exhausted"
684 );
685 let _ = audit_log.append(
686 AuditEventType::SecurityViolation,
687 "token_budget",
688 &format!("Token budget exhausted (remaining: {})", budget.remaining()),
689 Some(serde_json::json!({
690 "remaining": budget.remaining(),
691 "used": budget.used_tokens,
692 "iteration": iteration,
693 })),
694 );
695 return Err(crate::error::RavenClawsError::SecurityViolation(
696 "Token budget exhausted".to_string(),
697 ));
698 }
699 }
700
701 let response = match llm.chat(messages.clone()).await {
702 Ok(r) => r,
703 Err(e) => {
704 if let Some(ref chain) = config.fallback_chain {
706 warn!(error = %e, "Primary LLM failed, trying fallback chain");
707 let _ = audit_log.append(
708 AuditEventType::Error,
709 "llm",
710 &format!("Primary LLM failed, trying fallback: {}", e),
711 None,
712 );
713 let configs = {
715 let c = chain.lock().unwrap();
716 c.configs.clone()
717 };
718 let mut temp_chain = ProviderFallbackChain::new(configs);
719 match temp_chain.chat_with_fallback(messages).await {
720 Ok(r) => {
721 info!("Fallback chain succeeded");
722 if let Some(ref budget) = config.token_budget {
724 if let Some(usage) = &r.usage {
725 let mut b = budget.lock().unwrap();
726 b.record_usage(usage.total_tokens);
727 }
728 }
729 r
730 }
731 Err(fallback_e) => {
732 warn!(error = %fallback_e, "Fallback chain also failed");
733 let _ = audit_log.append(
734 AuditEventType::Error,
735 "llm",
736 &format!("All providers failed: {}", fallback_e),
737 None,
738 );
739 return Err(crate::error::RavenClawsError::Llm(fallback_e));
740 }
741 }
742 } else {
743 warn!(error = %e, "LLM request failed");
744 let _ = audit_log.append(
745 AuditEventType::Error,
746 "llm",
747 &format!("LLM request failed: {}", e),
748 None,
749 );
750 return Err(crate::error::RavenClawsError::Llm(e));
751 }
752 }
753 };
754
755 if let Some(ref budget) = config.token_budget {
757 if let Some(usage) = &response.usage {
758 let mut b = budget.lock().unwrap();
759 b.record_usage(usage.total_tokens);
760 debug!(
761 iteration = iteration,
762 tokens_used = usage.total_tokens,
763 total_used = b.used_tokens,
764 remaining = b.remaining(),
765 "Token usage recorded"
766 );
767 }
768 }
769
770 if let Some(ref rf) = config.ravenfabric {
772 if rf.is_enabled() {
773 let _ = rf.health().await;
774 info!(
775 iteration = iteration,
776 ravenfabric = true,
777 "RavenFabric health check completed"
778 );
779 }
780 }
781
782 let first_choice = response.choices.first();
783 let content = first_choice
784 .map(|c| c.message.content.clone())
785 .unwrap_or_default();
786
787 debug!(
788 iteration = iteration,
789 response_length = content.len(),
790 response_preview = %content[..content.len().min(500)],
791 "LLM response received (MCP loop)"
792 );
793
794 if let Some(ref detector) = injection_detector {
796 match detector.check(&content) {
797 crate::policy::InjectionVerdict::Suspicious(reason) => {
798 warn!(
799 iteration = iteration,
800 reason = %reason,
801 "Prompt-injection detected in LLM response"
802 );
803 let _ = audit_log.append(
804 AuditEventType::SecurityViolation,
805 "injection_detector",
806 &format!("Prompt-injection detected: {}", reason),
807 Some(serde_json::json!({
808 "reason": reason,
809 "iteration": iteration,
810 "content_preview": &content[..content.len().min(200)],
811 })),
812 );
813 return Err(crate::error::RavenClawsError::SecurityViolation(format!(
814 "LLM response blocked: potential prompt injection ({})",
815 reason
816 )));
817 }
818 crate::policy::InjectionVerdict::Clean => {}
819 }
820 }
821
822 if config.enable_tools {
824 if let Some((tool_name, args)) = first_choice.and_then(parse_structured_tool_call) {
825 info!(tool = %tool_name, "Structured tool call detected");
826
827 if let Some(tool_result) = execute_parsed_tool_call(
829 tool_name,
830 args,
831 ®istry,
832 &policy_engine,
833 &sandbox,
834 &audit_log,
835 config.require_approval,
836 )
837 .await
838 {
839 let observation = if tool_result.success {
840 format!("OBSERVATION: {}", tool_result.output)
841 } else {
842 format!(
843 "OBSERVATION: Tool failed with error: {}",
844 tool_result.error.as_deref().unwrap_or("unknown error")
845 )
846 };
847
848 memory.add_user_message(&observation);
849
850 info!(
851 iteration = iteration,
852 tool = %tool_result.tool_name,
853 success = tool_result.success,
854 "Structured tool executed"
855 );
856 continue;
857 }
858 }
859 }
860
861 if content.contains("FINAL:") {
863 let final_response = content
864 .split("FINAL:")
865 .nth(1)
866 .unwrap_or("")
867 .trim()
868 .to_string();
869
870 memory.add_assistant_message(&content);
871
872 let _ = audit_log.append(
874 AuditEventType::AgentFinish,
875 "agent",
876 "Agent loop completed successfully",
877 Some(serde_json::json!({
878 "iterations": iteration + 1,
879 "final_response_length": final_response.len(),
880 })),
881 );
882
883 return Ok(final_response);
884 }
885
886 if config.enable_tools {
888 if let Some(tool_result) = execute_tool_call_with_security(
889 &content,
890 ®istry,
891 &policy_engine,
892 &sandbox,
893 &audit_log,
894 )
895 .await
896 {
897 let observation = if tool_result.success {
898 format!("OBSERVATION: {}", tool_result.output)
899 } else {
900 format!(
901 "OBSERVATION: Tool failed with error: {}",
902 tool_result.error.as_deref().unwrap_or("unknown error")
903 )
904 };
905
906 memory.add_assistant_message(&content);
907 memory.add_user_message(&observation);
908
909 info!(
910 iteration = iteration,
911 tool = %tool_result.tool_name,
912 success = tool_result.success,
913 "Tool executed"
914 );
915 continue;
916 }
917 }
918
919 memory.add_assistant_message(&content);
921
922 if config.no_final_required {
924 info!(
925 iteration = iteration,
926 response_length = content.len(),
927 "no_final_required: treating response as completion"
928 );
929 let _ = audit_log.append(
930 AuditEventType::AgentFinish,
931 "agent",
932 "Agent loop completed (no_final_required)",
933 Some(serde_json::json!({
934 "iterations": iteration + 1,
935 "final_response_length": content.len(),
936 })),
937 );
938 return Ok(content);
939 }
940
941 info!(
942 iteration = iteration,
943 thought = %content.lines().find(|l| l.starts_with("THOUGHT:")).unwrap_or("<no thought>"),
944 "Agent loop progress"
945 );
946 }
947
948 warn!(
950 max_iterations = config.max_iterations,
951 "Agent loop reached max iterations"
952 );
953
954 let _ = audit_log.append(
955 AuditEventType::Error,
956 "agent",
957 "Agent loop reached max iterations without completing",
958 Some(serde_json::json!({
959 "max_iterations": config.max_iterations,
960 })),
961 );
962
963 let history = memory.history();
964 if history.len() > 1 {
965 if let Some(last) = history.last() {
966 return Ok(last.content.clone());
967 }
968 }
969
970 Err(crate::error::RavenClawsError::CommandExecution(
971 "Agent loop reached max iterations without completing the task".to_string(),
972 ))
973}
974
975async fn prompt_for_approval(tool_name: &str, args: &serde_json::Value) -> bool {
980 use std::io::{IsTerminal, Write};
981
982 let args_str = serde_json::to_string_pretty(args).unwrap_or_default();
983
984 if !std::io::stdin().is_terminal() {
986 warn!(
987 tool = %tool_name,
988 "stdin is not a TTY ā auto-approving tool call (use --require-approval only in interactive mode)"
989 );
990 return true;
991 }
992
993 eprintln!("\nā ļø Tool requires approval:");
995 eprintln!(" Tool: {}", tool_name);
996 for line in args_str.lines() {
997 eprintln!(" {}", line);
998 }
999 eprint!(" Approve? [y/N] ");
1000 std::io::stderr().flush().ok();
1001
1002 let mut input = String::new();
1003 match std::io::stdin().read_line(&mut input) {
1004 Ok(_) => {
1005 let trimmed = input.trim().to_lowercase();
1006 trimmed == "y" || trimmed == "yes"
1007 }
1008 Err(e) => {
1009 warn!(error = %e, "Failed to read approval input ā denying by default");
1010 false
1011 }
1012 }
1013}
1014
1015#[cfg(test)]
1018async fn prompt_for_approval_with_input(
1019 tool_name: &str,
1020 args: &serde_json::Value,
1021 input: &str,
1022) -> bool {
1023 use std::io::Write;
1024
1025 let args_str = serde_json::to_string_pretty(args).unwrap_or_default();
1026
1027 eprintln!("\nā ļø Tool requires approval:");
1028 eprintln!(" Tool: {}", tool_name);
1029 for line in args_str.lines() {
1030 eprintln!(" {}", line);
1031 }
1032 eprint!(" Approve? [y/N] ");
1033 std::io::stderr().flush().ok();
1034
1035 let trimmed = input.trim().to_lowercase();
1036 trimmed == "y" || trimmed == "yes"
1037}
1038
1039async fn execute_parsed_tool_call(
1048 tool_name: String,
1049 args: serde_json::Value,
1050 registry: &ToolRegistry,
1051 policy_engine: &PolicyEngine,
1052 _sandbox: &Sandbox,
1053 audit_log: &AuditLog,
1054 require_approval: bool,
1055) -> Option<ToolResult> {
1056 info!(tool = %tool_name, "Executing parsed tool call");
1057
1058 let _ = audit_log.tool_call(&tool_name, &args);
1060
1061 if require_approval && policy_engine.requires_approval(&tool_name) {
1063 let _ = audit_log.append(
1064 AuditEventType::ApprovalRequested,
1065 "approval",
1066 &format!("Approval required for tool: {}", tool_name),
1067 Some(serde_json::json!({"tool": tool_name, "args": args})),
1068 );
1069
1070 let granted = prompt_for_approval(&tool_name, &args).await;
1072
1073 if !granted {
1074 let _ = audit_log.approval(&tool_name, false, Some("Denied by user"));
1075 warn!(tool = %tool_name, "Tool call denied by user");
1076 return Some(ToolResult {
1077 tool_name: tool_name.clone(),
1078 success: false,
1079 output: String::new(),
1080 error: Some(format!("Approval denied by user for tool: {}", tool_name)),
1081 exit_code: Some(-1),
1082 duration_ms: None,
1083 });
1084 }
1085
1086 let _ = audit_log.approval(&tool_name, true, Some("Approved by user"));
1087 info!(tool = %tool_name, "Tool call approved by user");
1088 }
1089
1090 let policy_decision = policy_engine.check_tool_call(&tool_name, &args);
1092
1093 match &policy_decision {
1095 Decision::Allow => {
1096 let _ = audit_log.policy_decision(&tool_name, true, None);
1097 }
1098 Decision::Deny(reason) => {
1099 let _ = audit_log.policy_decision(&tool_name, false, Some(reason));
1100 warn!(tool = %tool_name, reason = %reason, "Tool call denied by policy");
1101 return Some(ToolResult {
1102 tool_name: tool_name.clone(),
1103 success: false,
1104 output: String::new(),
1105 error: Some(format!("Policy denied: {}", reason)),
1106 exit_code: Some(-1),
1107 duration_ms: None,
1108 });
1109 }
1110 }
1111
1112 let tool_name_clone = tool_name.clone();
1114 let call = ToolCall {
1115 name: tool_name.clone(),
1116 arguments: args,
1117 id: None,
1118 };
1119
1120 let result = match registry.execute(call).await {
1121 Ok(result) => {
1122 let _ = audit_log.append(
1124 AuditEventType::ToolResult,
1125 &tool_name_clone,
1126 &format!(
1127 "Tool executed: {} (success: {})",
1128 tool_name_clone, result.success
1129 ),
1130 Some(serde_json::json!({
1131 "success": result.success,
1132 "exit_code": result.exit_code,
1133 "duration_ms": result.duration_ms,
1134 })),
1135 );
1136 result
1137 }
1138 Err(e) => {
1139 let _ = audit_log.append(
1141 AuditEventType::Error,
1142 &tool_name_clone,
1143 &format!("Tool execution failed: {}", e),
1144 None,
1145 );
1146 ToolResult {
1147 tool_name: tool_name_clone,
1148 success: false,
1149 output: String::new(),
1150 error: Some(e.to_string()),
1151 exit_code: Some(-1),
1152 duration_ms: None,
1153 }
1154 }
1155 };
1156
1157 Some(result)
1158}
1159
1160async fn execute_tool_call_with_security(
1169 content: &str,
1170 registry: &ToolRegistry,
1171 policy_engine: &PolicyEngine,
1172 _sandbox: &Sandbox,
1173 audit_log: &AuditLog,
1174) -> Option<ToolResult> {
1175 let (tool_name, args) = parse_tool_call(content)?;
1177
1178 execute_parsed_tool_call(
1180 tool_name,
1181 args,
1182 registry,
1183 policy_engine,
1184 _sandbox,
1185 audit_log,
1186 false, )
1188 .await
1189}
1190
1191fn parse_structured_tool_call(choice: &Choice) -> Option<(String, serde_json::Value)> {
1195 let tool_calls = choice.tool_calls.as_ref()?;
1196 let first_call = tool_calls.first()?;
1197
1198 let tool_name = first_call.function.name.clone();
1199 let args: serde_json::Value = serde_json::from_str(&first_call.function.arguments).ok()?;
1200
1201 Some((tool_name, args))
1202}
1203
1204fn parse_tool_call(content: &str) -> Option<(String, serde_json::Value)> {
1206 let mut lines = content.lines();
1207 let tool_call_line = lines.find(|l| l.trim().starts_with("TOOL_CALL:"))?;
1208
1209 let tool_name = tool_call_line
1210 .trim()
1211 .strip_prefix("TOOL_CALL:")
1212 .map(|s| s.trim())
1213 .filter(|s| !s.is_empty())?
1214 .to_string();
1215
1216 let args_line = lines.find(|l| l.trim().starts_with("ARGS:"))?;
1218 let args_str = args_line.trim().strip_prefix("ARGS:").map(|s| s.trim())?;
1219
1220 let args: serde_json::Value = serde_json::from_str(args_str).ok()?;
1221
1222 Some((tool_name, args))
1223}
1224
1225pub async fn run_single(
1227 llm: Arc<dyn LLMProviderTrait>,
1228 config: Config,
1229 ravenfabric: Option<RavenFabricClient>,
1230) -> Result<()> {
1231 info!(
1232 "Starting single agent mode with provider: {}",
1233 llm.provider_name()
1234 );
1235
1236 if let Some(ref rf) = ravenfabric {
1238 if rf.is_enabled() {
1239 info!("RavenFabric remote execution available");
1240 match rf.health().await {
1241 Ok(true) => info!("RavenFabric mesh is healthy"),
1242 Ok(false) => warn!("RavenFabric mesh returned unhealthy status"),
1243 Err(e) => warn!(error = %e, "RavenFabric health check failed"),
1244 }
1245 }
1246 }
1247
1248 let system_prompt = &config.llm.system_prompt;
1249
1250 let messages = vec![
1251 ChatMessage {
1252 role: "system".to_string(),
1253 content: system_prompt.to_string(),
1254 },
1255 ChatMessage {
1256 role: "user".to_string(),
1257 content: "Ready. Awaiting instructions.".to_string(),
1258 },
1259 ];
1260
1261 match llm.chat(messages).await {
1262 Ok(response) => {
1263 if let Some(choice) = response.choices.first() {
1264 info!(provider = llm.provider_name(), model = llm.model(), response = %choice.message.content, "Agent response received");
1265
1266 if let Some(ref rf) = ravenfabric {
1268 if rf.is_enabled() {
1269 let preview = choice.message.content.chars().take(500).collect::<String>();
1270 let _ = rf.broadcast(&preview, 30).await;
1271 info!("Agent result broadcast to RavenFabric mesh");
1272 }
1273 }
1274 }
1275 }
1276 Err(e) => {
1277 warn!(error = %e, provider = llm.provider_name(), "LLM request failed");
1278 }
1279 }
1280
1281 Ok(())
1282}
1283
1284pub async fn run_swarm(
1289 llm: Arc<dyn LLMProviderTrait>,
1290 config: Config,
1291 ravenfabric: Option<RavenFabricClient>,
1292) -> Result<()> {
1293 info!("Starting swarm mode (single-provider) ā 3 parallel agents");
1294
1295 if let Some(ref rf) = ravenfabric {
1297 if rf.is_enabled() {
1298 info!("RavenFabric remote execution available for swarm coordination");
1299 match rf.health().await {
1300 Ok(true) => info!("RavenFabric mesh is healthy"),
1301 Ok(false) => warn!("RavenFabric mesh returned unhealthy status"),
1302 Err(e) => warn!(error = %e, "RavenFabric health check failed"),
1303 }
1304 }
1305 }
1306
1307 let _system_prompt = &config.llm.system_prompt;
1308 let num_agents = 3;
1309 let mut handles = Vec::new();
1310
1311 let personas = [
1313 "You are an analytical agent. Focus on logic, structure, and precision.",
1314 "You are a creative agent. Focus on innovation, alternatives, and possibilities.",
1315 "You are a pragmatic agent. Focus on simplicity, efficiency, and practicality.",
1316 ];
1317
1318 for (i, persona) in personas.iter().enumerate().take(num_agents) {
1319 let llm_clone = llm.clone();
1320 let persona = persona.to_string();
1321 let task = "Analyze the given task and provide your solution.".to_string();
1322
1323 let handle = tokio::spawn(async move {
1324 let mut memory = ConversationMemory::new(&persona, 10);
1325 memory.add_user_message(&task);
1326
1327 let messages = memory.history().to_vec();
1328 match llm_clone.chat(messages).await {
1329 Ok(response) => {
1330 let content = response
1331 .choices
1332 .first()
1333 .map(|c| c.message.content.clone())
1334 .unwrap_or_default();
1335 Ok((i, content))
1336 }
1337 Err(e) => Err(format!("Agent {} failed: {}", i, e)),
1338 }
1339 });
1340
1341 handles.push(handle);
1342 }
1343
1344 let mut results: Vec<(usize, String)> = Vec::new();
1346 for handle in handles {
1347 match handle.await {
1348 Ok(Ok((idx, result))) => {
1349 info!("Agent {} completed: {} chars", idx, result.len());
1350 results.push((idx, result));
1351 }
1352 Ok(Err(e)) => warn!("Agent failed: {}", e),
1353 Err(e) => warn!("Agent join failed: {}", e),
1354 }
1355 }
1356
1357 println!("\nš¦ā⬠Swarm Results ({} agents):", results.len());
1359 for (idx, result) in &results {
1360 println!(
1361 "\nāā Agent {} ({}) āā",
1362 idx + 1,
1363 personas[*idx].split('.').next().unwrap_or("Unknown")
1364 );
1365 println!("{}", result);
1366 }
1367
1368 if let Some(ref rf) = ravenfabric {
1370 if rf.is_enabled() {
1371 let summary = format!(
1372 "Swarm completed: {} agents, results: {}",
1373 results.len(),
1374 results
1375 .iter()
1376 .map(|(i, r)| format!("Agent {}: {} chars", i, r.len()))
1377 .collect::<Vec<_>>()
1378 .join(", ")
1379 );
1380 let _ = rf.broadcast(&summary, 30).await;
1381 info!("Swarm results broadcast to RavenFabric mesh");
1382 }
1383 }
1384
1385 Ok(())
1386}
1387
1388pub async fn run_supervisor(
1393 llm: Arc<dyn LLMProviderTrait>,
1394 config: Config,
1395 ravenfabric: Option<RavenFabricClient>,
1396) -> Result<()> {
1397 info!("Starting supervisor mode (single-provider)");
1398
1399 if let Some(ref rf) = ravenfabric {
1401 if rf.is_enabled() {
1402 info!("RavenFabric remote execution available for supervisor coordination");
1403 match rf.health().await {
1404 Ok(true) => info!("RavenFabric mesh is healthy"),
1405 Ok(false) => warn!("RavenFabric mesh returned unhealthy status"),
1406 Err(e) => warn!(error = %e, "RavenFabric health check failed"),
1407 }
1408 }
1409 }
1410
1411 let system_prompt = &config.llm.system_prompt;
1412 let policy_engine = PolicyEngine::default_secure();
1413 let mut sandbox = Sandbox::default();
1414 sandbox.init().await.map_err(|e| {
1415 crate::error::RavenClawsError::CommandExecution(format!("Sandbox init failed: {}", e))
1416 })?;
1417 let audit_log = AuditLog::new(format!("supervisor-{}", std::process::id()));
1418 let registry = ToolRegistry::with_default_tools();
1419
1420 let supervisor_prompt = format!(
1422 "You are a supervisor agent. Your task is to decompose complex tasks into subtasks \
1423 and coordinate sub-agents to complete them. \
1424 \n\nFor each subtask, respond with:\n\
1425 SUBTASK: <description>\n\
1426 AGENT: <agent_number>\n\
1427 \nWhen all subtasks are complete, respond with:\n\
1428 FINAL: <aggregated result>\n\
1429 \nTask: {}",
1430 "Coordinate the completion of the assigned task."
1431 );
1432
1433 let mut memory = ConversationMemory::new(system_prompt, 20);
1434 memory.add_user_message(&supervisor_prompt);
1435
1436 let mut subtask_results: Vec<String> = Vec::new();
1437 let mut iteration = 0;
1438 let max_iterations = 15;
1439
1440 loop {
1441 iteration += 1;
1442 if iteration > max_iterations {
1443 warn!("Supervisor reached max iterations");
1444 break;
1445 }
1446
1447 let messages = memory.history().to_vec();
1448 let response = match llm.chat(messages).await {
1449 Ok(r) => r,
1450 Err(e) => {
1451 warn!(error = %e, "Supervisor LLM request failed");
1452 continue;
1453 }
1454 };
1455
1456 let content = response
1457 .choices
1458 .first()
1459 .map(|c| c.message.content.clone())
1460 .unwrap_or_default();
1461
1462 if content.contains("FINAL:") {
1464 let final_response = content
1465 .split("FINAL:")
1466 .nth(1)
1467 .unwrap_or("")
1468 .trim()
1469 .to_string();
1470 info!("Supervisor completed task: {} chars", final_response.len());
1471
1472 let _ = audit_log.append(
1473 AuditEventType::AgentFinish,
1474 "supervisor",
1475 "Supervisor completed task coordination",
1476 Some(serde_json::json!({
1477 "iterations": iteration,
1478 "subtasks_completed": subtask_results.len(),
1479 })),
1480 );
1481
1482 println!("\nš¦ā⬠Supervisor Result:\n{}", final_response);
1483 return Ok(());
1484 }
1485
1486 if content.contains("SUBTASK:") {
1488 let subtask_block = content.split("SUBTASK:").nth(1).unwrap_or("");
1489 let subtask_lines: Vec<&str> = subtask_block.lines().take(3).collect();
1490
1491 let subtask_desc = subtask_lines.first().unwrap_or(&"").trim();
1492 let agent_num = subtask_lines
1493 .iter()
1494 .find(|l| l.starts_with("AGENT:"))
1495 .and_then(|l| l.split(':').nth(1))
1496 .unwrap_or("1")
1497 .trim();
1498
1499 if !subtask_desc.is_empty() {
1500 info!("Subtask {}: {}", agent_num, subtask_desc);
1501
1502 let subtask_result = run_subtask_agent(
1504 llm.clone(),
1505 subtask_desc,
1506 system_prompt,
1507 &policy_engine,
1508 &sandbox,
1509 &audit_log,
1510 ®istry,
1511 )
1512 .await;
1513
1514 match subtask_result {
1515 Ok(result) => {
1516 info!("Subtask {} completed: {} chars", agent_num, result.len());
1517 subtask_results.push(format!("Agent {} result: {}", agent_num, result));
1518
1519 memory.add_assistant_message(&format!(
1520 "Decomposed subtask {}: {}",
1521 agent_num, subtask_desc
1522 ));
1523 memory
1524 .add_user_message(&format!("Subtask {} result: {}", agent_num, result));
1525 }
1526 Err(e) => {
1527 warn!("Subtask {} failed: {}", agent_num, e);
1528 memory
1529 .add_assistant_message(&format!("Subtask {} failed: {}", agent_num, e));
1530 }
1531 }
1532 }
1533 } else {
1534 memory.add_assistant_message(&content);
1535 }
1536 }
1537
1538 if !subtask_results.is_empty() {
1540 let aggregated = subtask_results.join("\n\n");
1541 info!(
1542 "Supervisor aggregated {} subtask results",
1543 subtask_results.len()
1544 );
1545
1546 if let Some(ref rf) = ravenfabric {
1548 if rf.is_enabled() {
1549 let summary = format!(
1550 "Supervisor completed: {} subtasks, result: {} chars",
1551 subtask_results.len(),
1552 aggregated.len()
1553 );
1554 let _ = rf.broadcast(&summary, 30).await;
1555 info!("Supervisor result broadcast to RavenFabric mesh");
1556 }
1557 }
1558
1559 println!("\nš¦ā⬠Supervisor Aggregated Result:\n{}", aggregated);
1560 return Ok(());
1561 }
1562
1563 Err(crate::error::RavenClawsError::CommandExecution(
1564 "Supervisor mode completed without results".to_string(),
1565 ))
1566}
1567
1568async fn run_subtask_agent(
1570 llm: Arc<dyn LLMProviderTrait>,
1571 subtask: &str,
1572 system_prompt: &str,
1573 policy_engine: &PolicyEngine,
1574 sandbox: &Sandbox,
1575 audit_log: &AuditLog,
1576 registry: &ToolRegistry,
1577) -> Result<String> {
1578 let mut memory = ConversationMemory::new(system_prompt, 10);
1579 memory.add_user_message(&format!("Execute this subtask: {}", subtask));
1580
1581 for i in 0..5 {
1582 let messages = memory.history().to_vec();
1583 let response = match llm.chat(messages).await {
1584 Ok(r) => r,
1585 Err(e) => {
1586 warn!(error = %e, iteration = i, "Subtask agent LLM failed");
1587 continue;
1588 }
1589 };
1590
1591 let content = response
1592 .choices
1593 .first()
1594 .map(|c| c.message.content.clone())
1595 .unwrap_or_default();
1596
1597 if content.contains("FINAL:") || content.contains("DONE:") {
1598 return Ok(content
1599 .replace("FINAL:", "")
1600 .replace("DONE:", "")
1601 .trim()
1602 .to_string());
1603 }
1604
1605 if let Some(tool_result) =
1607 execute_tool_call_with_security(&content, registry, policy_engine, sandbox, audit_log)
1608 .await
1609 {
1610 memory.add_assistant_message(&content);
1611 memory.add_user_message(&format!("Tool result: {}", tool_result.output));
1612 } else {
1613 memory.add_assistant_message(&content);
1614 memory.add_user_message("Continue with next step.");
1615 }
1616 }
1617
1618 Ok("Subtask completed".to_string())
1619}
1620
1621pub async fn run_single_multi(
1623 multi_llm: MultiModelManager,
1624 config: Config,
1625 ravenfabric: Option<RavenFabricClient>,
1626) -> Result<()> {
1627 info!(
1628 "Starting single agent mode (multi-model) with {} providers",
1629 multi_llm.client_count()
1630 );
1631
1632 if let Some(ref rf) = ravenfabric {
1634 if rf.is_enabled() {
1635 info!("RavenFabric remote execution available");
1636 match rf.health().await {
1637 Ok(true) => info!("RavenFabric mesh is healthy"),
1638 Ok(false) => warn!("RavenFabric mesh returned unhealthy status"),
1639 Err(e) => warn!(error = %e, "RavenFabric health check failed"),
1640 }
1641 }
1642 }
1643
1644 let system_prompt = &config.llm.system_prompt;
1645
1646 let messages = vec![
1647 ChatMessage {
1648 role: "system".to_string(),
1649 content: system_prompt.to_string(),
1650 },
1651 ChatMessage {
1652 role: "user".to_string(),
1653 content: "Ready. Awaiting instructions.".to_string(),
1654 },
1655 ];
1656
1657 let mut last_index = 0;
1659 for i in 0..multi_llm.client_count() {
1660 let client = if i == 0 {
1661 multi_llm.get_client(0)
1662 } else {
1663 multi_llm.next_client(last_index)
1664 };
1665
1666 if let Some(client) = client {
1667 match client.chat(messages.clone()).await {
1668 Ok(response) => {
1669 if let Some(choice) = response.choices.first() {
1670 info!(provider = client.provider_name(), model = client.model(), response = %choice.message.content, "Provider response received");
1671 }
1672 }
1673 Err(e) => {
1674 warn!(error = %e, provider = client.provider_name(), model = client.model(), "Provider request failed");
1675 }
1676 }
1677 last_index = i;
1678 }
1679 }
1680
1681 if let Some(ref rf) = ravenfabric {
1683 if rf.is_enabled() {
1684 let _ = rf
1685 .broadcast("Single agent (multi-model) completed", 30)
1686 .await;
1687 info!("Multi-model result broadcast to RavenFabric mesh");
1688 }
1689 }
1690
1691 Ok(())
1692}
1693
1694pub async fn run_swarm_multi(
1699 multi_llm: MultiModelManager,
1700 config: Config,
1701 ravenfabric: Option<RavenFabricClient>,
1702) -> Result<()> {
1703 info!(
1704 "Starting swarm mode (multi-model) ā {} parallel agents",
1705 multi_llm.client_count()
1706 );
1707
1708 if let Some(ref rf) = ravenfabric {
1710 if rf.is_enabled() {
1711 info!("RavenFabric remote execution available for swarm coordination");
1712 match rf.health().await {
1713 Ok(true) => info!("RavenFabric mesh is healthy"),
1714 Ok(false) => warn!("RavenFabric mesh returned unhealthy status"),
1715 Err(e) => warn!(error = %e, "RavenFabric health check failed"),
1716 }
1717 }
1718 }
1719
1720 let _system_prompt = &config.llm.system_prompt;
1721 let num_agents = multi_llm.client_count().min(3); let mut handles = Vec::new();
1723
1724 let personas = [
1726 "You are an analytical agent. Focus on logic, structure, and precision.",
1727 "You are a creative agent. Focus on innovation, alternatives, and possibilities.",
1728 "You are a pragmatic agent. Focus on simplicity, efficiency, and practicality.",
1729 ];
1730
1731 for i in 0..num_agents {
1732 let client = multi_llm.get_client(i).unwrap().clone();
1733 let persona = personas.get(i).unwrap_or(&personas[0]).to_string();
1734 let task = "Analyze the given task and provide your solution.".to_string();
1735
1736 let handle = tokio::spawn(async move {
1737 let mut memory = ConversationMemory::new(&persona, 10);
1738 memory.add_user_message(&task);
1739
1740 let messages = memory.history().to_vec();
1741 match client.chat(messages).await {
1742 Ok(response) => {
1743 let content = response
1744 .choices
1745 .first()
1746 .map(|c| c.message.content.clone())
1747 .unwrap_or_default();
1748 Ok((
1749 i,
1750 client.provider_name().to_string(),
1751 client.model().to_string(),
1752 content,
1753 ))
1754 }
1755 Err(e) => Err(format!("Agent {} failed: {}", i, e)),
1756 }
1757 });
1758
1759 handles.push(handle);
1760 }
1761
1762 let mut results: Vec<(usize, String, String, String)> = Vec::new();
1764 for handle in handles {
1765 match handle.await {
1766 Ok(Ok((idx, provider, model, result))) => {
1767 info!(
1768 "Agent {} ({}:{}) completed: {} chars",
1769 idx,
1770 provider,
1771 model,
1772 result.len()
1773 );
1774 results.push((idx, provider, model, result));
1775 }
1776 Ok(Err(e)) => warn!("Agent failed: {}", e),
1777 Err(e) => warn!("Agent join failed: {}", e),
1778 }
1779 }
1780
1781 println!(
1783 "\nš¦ā⬠Swarm Results ({} agents, multi-model):",
1784 results.len()
1785 );
1786 for (idx, provider, model, result) in &results {
1787 println!("\nāā Agent {} ({}:{}) āā", idx + 1, provider, model);
1788 println!("{}", result);
1789 }
1790
1791 if let Some(ref rf) = ravenfabric {
1793 if rf.is_enabled() {
1794 let summary = format!("Multi-model swarm completed: {} agents", results.len());
1795 let _ = rf.broadcast(&summary, 30).await;
1796 info!("Multi-model swarm results broadcast to RavenFabric mesh");
1797 }
1798 }
1799
1800 Ok(())
1801}
1802
1803pub async fn run_supervisor_multi(
1808 multi_llm: MultiModelManager,
1809 config: Config,
1810 ravenfabric: Option<RavenFabricClient>,
1811) -> Result<()> {
1812 info!(
1813 "Starting supervisor mode (multi-model) with {} providers",
1814 multi_llm.client_count()
1815 );
1816
1817 if let Some(ref rf) = ravenfabric {
1819 if rf.is_enabled() {
1820 info!("RavenFabric remote execution available for supervisor coordination");
1821 match rf.health().await {
1822 Ok(true) => info!("RavenFabric mesh is healthy"),
1823 Ok(false) => warn!("RavenFabric mesh returned unhealthy status"),
1824 Err(e) => warn!(error = %e, "RavenFabric health check failed"),
1825 }
1826 }
1827 }
1828
1829 let system_prompt = &config.llm.system_prompt;
1830 let policy_engine = PolicyEngine::default_secure();
1831 let mut sandbox = Sandbox::default();
1832 sandbox.init().await.map_err(|e| {
1833 crate::error::RavenClawsError::CommandExecution(format!("Sandbox init failed: {}", e))
1834 })?;
1835 let audit_log = AuditLog::new(format!("supervisor-multi-{}", std::process::id()));
1836 let registry = ToolRegistry::with_default_tools();
1837
1838 let supervisor_prompt = format!(
1840 "You are a supervisor agent coordinating multiple LLM providers. \
1841 Decompose tasks and assign them to appropriate providers based on their strengths. \
1842 \n\nFor each subtask, respond with:\n\
1843 SUBTASK: <description>\n\
1844 PROVIDER: <provider_index 0-{}>\n\
1845 \nWhen complete, respond with:\n\
1846 FINAL: <aggregated result>\n\
1847 \nTask: {}",
1848 multi_llm.client_count() - 1,
1849 "Coordinate the completion of the assigned task using available providers."
1850 );
1851
1852 let mut memory = ConversationMemory::new(system_prompt, 20);
1853 memory.add_user_message(&supervisor_prompt);
1854
1855 let mut subtask_results: Vec<String> = Vec::new();
1856 let mut iteration = 0;
1857 let max_iterations = 15;
1858
1859 loop {
1860 iteration += 1;
1861 if iteration > max_iterations {
1862 warn!("Supervisor reached max iterations");
1863 break;
1864 }
1865
1866 let supervisor_client = multi_llm
1868 .get_client(iteration % multi_llm.client_count())
1869 .or_else(|| multi_llm.get_client(0))
1870 .cloned();
1871
1872 let messages = memory.history().to_vec();
1873 let response =
1874 match supervisor_client.map(|c| tokio::spawn(async move { c.chat(messages).await })) {
1875 Some(handle) => match handle.await {
1876 Ok(Ok(r)) => r,
1877 Ok(Err(e)) => {
1878 warn!(error = %e, "Supervisor LLM request failed");
1879 continue;
1880 }
1881 Err(e) => {
1882 warn!(error = %e, "Supervisor task join failed");
1883 continue;
1884 }
1885 },
1886 None => {
1887 warn!("No LLM clients available");
1888 break;
1889 }
1890 };
1891
1892 let content = response
1893 .choices
1894 .first()
1895 .map(|c| c.message.content.clone())
1896 .unwrap_or_default();
1897
1898 if content.contains("FINAL:") {
1900 let final_response = content
1901 .split("FINAL:")
1902 .nth(1)
1903 .unwrap_or("")
1904 .trim()
1905 .to_string();
1906 info!("Supervisor completed task: {} chars", final_response.len());
1907
1908 let _ = audit_log.append(
1909 AuditEventType::AgentFinish,
1910 "supervisor",
1911 "Supervisor completed task coordination",
1912 Some(serde_json::json!({
1913 "iterations": iteration,
1914 "subtasks_completed": subtask_results.len(),
1915 "providers_used": multi_llm.client_count(),
1916 })),
1917 );
1918
1919 println!("\nš¦ā⬠Supervisor Result (multi-model):\n{}", final_response);
1920 return Ok(());
1921 }
1922
1923 if content.contains("SUBTASK:") && content.contains("PROVIDER:") {
1925 let subtask_block = content.split("SUBTASK:").nth(1).unwrap_or("");
1926 let subtask_lines: Vec<&str> = subtask_block.lines().take(4).collect();
1927
1928 let subtask_desc = subtask_lines.first().unwrap_or(&"").trim();
1929 let provider_idx = subtask_lines
1930 .iter()
1931 .find(|l| l.starts_with("PROVIDER:"))
1932 .and_then(|l| l.split(':').nth(1))
1933 .and_then(|s| s.trim().parse::<usize>().ok())
1934 .unwrap_or(0);
1935
1936 if !subtask_desc.is_empty() {
1937 info!("Subtask for provider {}: {}", provider_idx, subtask_desc);
1938
1939 let client = multi_llm
1940 .get_client(provider_idx)
1941 .or_else(|| multi_llm.get_client(0));
1942
1943 if let Some(client) = client {
1944 let subtask_result = run_subtask_agent(
1945 client.clone(),
1946 subtask_desc,
1947 system_prompt,
1948 &policy_engine,
1949 &sandbox,
1950 &audit_log,
1951 ®istry,
1952 )
1953 .await;
1954
1955 match subtask_result {
1956 Ok(result) => {
1957 info!("Subtask {} completed: {} chars", provider_idx, result.len());
1958 subtask_results.push(format!(
1959 "Provider {} ({}): {}",
1960 provider_idx,
1961 client.provider_name(),
1962 result
1963 ));
1964
1965 memory.add_assistant_message(&format!(
1966 "Assigned subtask to provider {}: {}",
1967 provider_idx, subtask_desc
1968 ));
1969 memory.add_user_message(&format!(
1970 "Provider {} result: {}",
1971 provider_idx, result
1972 ));
1973 }
1974 Err(e) => {
1975 warn!("Subtask {} failed: {}", provider_idx, e);
1976 memory.add_assistant_message(&format!(
1977 "Provider {} subtask failed: {}",
1978 provider_idx, e
1979 ));
1980 }
1981 }
1982 }
1983 }
1984 } else {
1985 memory.add_assistant_message(&content);
1986 }
1987 }
1988
1989 if !subtask_results.is_empty() {
1991 let aggregated = subtask_results.join("\n\n");
1992 info!(
1993 "Supervisor aggregated {} subtask results",
1994 subtask_results.len()
1995 );
1996
1997 if let Some(ref rf) = ravenfabric {
1999 if rf.is_enabled() {
2000 let summary = format!(
2001 "Multi-model supervisor completed: {} subtasks, result: {} chars",
2002 subtask_results.len(),
2003 aggregated.len()
2004 );
2005 let _ = rf.broadcast(&summary, 30).await;
2006 info!("Multi-model supervisor result broadcast to RavenFabric mesh");
2007 }
2008 }
2009
2010 println!(
2011 "\nš¦ā⬠Supervisor Aggregated Result (multi-model):\n{}",
2012 aggregated
2013 );
2014 return Ok(());
2015 }
2016
2017 Err(crate::error::RavenClawsError::CommandExecution(
2018 "Supervisor mode completed without results".to_string(),
2019 ))
2020}
2021
2022pub async fn run_repl(llm: Arc<dyn LLMProviderTrait>, config: Config) -> Result<()> {
2024 use tokio::io::{AsyncBufReadExt, BufReader};
2025
2026 info!("Starting interactive REPL mode");
2027
2028 let system_prompt = &config.llm.system_prompt;
2029 let mut memory = ConversationMemory::new(system_prompt, 0);
2030
2031 let stdin = BufReader::new(tokio::io::stdin());
2032 let mut lines = stdin.lines();
2033
2034 println!("RavenClaws REPL ā type /exit to quit, /reset to clear history");
2035
2036 loop {
2037 print!("\n> ");
2038 use tokio::io::AsyncWriteExt;
2039 tokio::io::stdout().flush().await?;
2040
2041 let line = match lines.next_line().await {
2042 Ok(Some(l)) => l,
2043 Ok(None) => break, Err(e) => {
2045 warn!(error = %e, "REPL read error");
2046 break;
2047 }
2048 };
2049
2050 let input = line.trim();
2051
2052 if input.is_empty() {
2053 continue;
2054 }
2055
2056 match input {
2057 "/exit" | "/quit" => {
2058 println!("Exiting REPL.");
2059 break;
2060 }
2061 "/reset" => {
2062 memory = ConversationMemory::new(system_prompt, 0);
2063 println!("Conversation history reset.");
2064 continue;
2065 }
2066 _ => {}
2067 }
2068
2069 memory.add_user_message(input);
2070 let messages = memory.history().to_vec();
2071
2072 match llm.chat(messages).await {
2073 Ok(response) => {
2074 if let Some(choice) = response.choices.first() {
2075 let content = &choice.message.content;
2076 println!("{}", content);
2077 memory.add_assistant_message(content);
2078 }
2079 }
2080 Err(e) => {
2081 warn!(error = %e, "LLM request failed");
2082 println!("Error: {}", e);
2083 }
2084 }
2085 }
2086
2087 Ok(())
2088}
2089
2090#[cfg(test)]
2091mod tests {
2092 use super::*;
2093
2094 #[test]
2095 fn test_swarm_function_exists() {
2096 let _fn_ptr: fn(Arc<dyn LLMProviderTrait>, Config, Option<RavenFabricClient>) -> _ =
2098 run_swarm;
2099 }
2100
2101 #[test]
2102 fn test_supervisor_function_exists() {
2103 let _fn_ptr: fn(Arc<dyn LLMProviderTrait>, Config, Option<RavenFabricClient>) -> _ =
2105 run_supervisor;
2106 }
2107
2108 #[test]
2109 fn test_conversation_memory_new() {
2110 let mem = ConversationMemory::new("system prompt", 10);
2111 assert_eq!(mem.messages.len(), 1);
2112 assert_eq!(mem.messages[0].role, "system");
2113 assert_eq!(mem.messages[0].content, "system prompt");
2114 }
2115
2116 #[test]
2117 fn test_conversation_memory_add_user() {
2118 let mut mem = ConversationMemory::new("system", 10);
2119 mem.add_user_message("hello");
2120 assert_eq!(mem.messages.len(), 2);
2121 assert_eq!(mem.messages[1].role, "user");
2122 assert_eq!(mem.messages[1].content, "hello");
2123 }
2124
2125 #[test]
2126 fn test_conversation_memory_trim() {
2127 let mut mem = ConversationMemory::new("system", 3);
2128 mem.add_user_message("msg1");
2129 mem.add_assistant_message("resp1");
2130 mem.add_user_message("msg2");
2131 mem.add_assistant_message("resp2");
2132 assert!(mem.messages.len() <= 3);
2134 }
2135
2136 #[test]
2137 fn test_parse_tool_call_valid() {
2138 let content = "THOUGHT: I need to run a command\nTOOL_CALL: shell_exec\nARGS: {\"command\": \"echo hello\"}";
2139 let (name, args) = parse_tool_call(content).unwrap();
2140 assert_eq!(name, "shell_exec");
2141 assert_eq!(args["command"], "echo hello");
2142 }
2143
2144 #[test]
2145 fn test_parse_tool_call_missing_tool() {
2146 let content = "THOUGHT: no tool here";
2147 assert!(parse_tool_call(content).is_none());
2148 }
2149
2150 #[test]
2151 fn test_parse_tool_call_missing_args() {
2152 let content = "TOOL_CALL: shell_exec\nNo args line";
2153 assert!(parse_tool_call(content).is_none());
2154 }
2155
2156 #[test]
2157 fn test_parse_tool_call_invalid_json() {
2158 let content = "TOOL_CALL: shell_exec\nARGS: not valid json";
2159 assert!(parse_tool_call(content).is_none());
2160 }
2161
2162 #[test]
2163 fn test_agent_loop_config_default() {
2164 let config = AgentLoopConfig::default();
2165 assert_eq!(config.max_iterations, 10);
2166 assert!(!config.enable_tools);
2167 assert!(!config.require_approval);
2168 }
2169
2170 #[test]
2171 fn test_agent_loop_config_require_approval() {
2172 let config = AgentLoopConfig {
2173 max_iterations: 5,
2174 enable_tools: true,
2175 require_approval: true,
2176 prompt_injection_protection: true,
2177 token_lifetime_secs: 0,
2178 no_final_required: false,
2179 fallback_chain: None,
2180 token_budget: None,
2181 ravenfabric: None,
2182 };
2183 assert_eq!(config.max_iterations, 5);
2184 assert!(config.enable_tools);
2185 assert!(config.require_approval);
2186 assert!(config.prompt_injection_protection);
2187 assert_eq!(config.token_lifetime_secs, 0);
2188 }
2189
2190 #[test]
2191 fn test_prompt_for_approval_yes() {
2192 let args = serde_json::json!({"command": "echo hello"});
2193 let result = tokio_test::block_on(prompt_for_approval_with_input("shell_exec", &args, "y"));
2194 assert!(result, "Should approve for 'y'");
2195 }
2196
2197 #[test]
2198 fn test_prompt_for_approval_yes_full() {
2199 let args = serde_json::json!({"command": "echo hello"});
2200 let result =
2201 tokio_test::block_on(prompt_for_approval_with_input("shell_exec", &args, "yes"));
2202 assert!(result, "Should approve for 'yes'");
2203 }
2204
2205 #[test]
2206 fn test_prompt_for_approval_no() {
2207 let args = serde_json::json!({"command": "echo hello"});
2208 let result = tokio_test::block_on(prompt_for_approval_with_input("shell_exec", &args, "n"));
2209 assert!(!result, "Should deny for 'n'");
2210 }
2211
2212 #[test]
2213 fn test_prompt_for_approval_no_full() {
2214 let args = serde_json::json!({"command": "echo hello"});
2215 let result =
2216 tokio_test::block_on(prompt_for_approval_with_input("shell_exec", &args, "no"));
2217 assert!(!result, "Should deny for 'no'");
2218 }
2219
2220 #[test]
2221 fn test_prompt_for_approval_empty() {
2222 let args = serde_json::json!({"command": "echo hello"});
2223 let result = tokio_test::block_on(prompt_for_approval_with_input("shell_exec", &args, ""));
2224 assert!(!result, "Should deny for empty input (default N)");
2225 }
2226
2227 #[test]
2228 fn test_prompt_for_approval_uppercase() {
2229 let args = serde_json::json!({"command": "echo hello"});
2230 let result = tokio_test::block_on(prompt_for_approval_with_input("shell_exec", &args, "Y"));
2231 assert!(result, "Should approve for uppercase 'Y'");
2232 }
2233
2234 #[test]
2235 fn test_prompt_for_approval_auto_approves_non_tty() {
2236 #[allow(clippy::let_underscore_future)]
2242 let _ = prompt_for_approval_with_input("test", &serde_json::json!({}), "y");
2243 }
2244
2245 #[test]
2246 fn test_execute_parsed_tool_call_skips_approval_when_not_required() {
2247 let registry = ToolRegistry::with_default_tools();
2248 let policy_engine = PolicyEngine::default_secure();
2249 let sandbox = Sandbox::default();
2250 let audit_log = AuditLog::new("test-session".to_string());
2251
2252 let args = serde_json::json!({"command": "echo hello"});
2253 let result = tokio_test::block_on(execute_parsed_tool_call(
2254 "shell_exec".to_string(),
2255 args,
2256 ®istry,
2257 &policy_engine,
2258 &sandbox,
2259 &audit_log,
2260 false, ));
2262
2263 assert!(result.is_some());
2264 let tool_result = result.unwrap();
2265 assert_eq!(tool_result.tool_name, "shell_exec");
2266 }
2267
2268 #[test]
2269 fn test_execute_parsed_tool_call_approval_not_needed_for_read_only_tools() {
2270 let registry = ToolRegistry::with_default_tools();
2273 let policy_engine = PolicyEngine::default_secure();
2274 let sandbox = Sandbox::default();
2275 let audit_log = AuditLog::new("test-session".to_string());
2276
2277 let args = serde_json::json!({"path": "/tmp/test.txt"});
2278 let result = tokio_test::block_on(execute_parsed_tool_call(
2279 "read_file".to_string(),
2280 args,
2281 ®istry,
2282 &policy_engine,
2283 &sandbox,
2284 &audit_log,
2285 true, ));
2287
2288 assert!(result.is_some());
2290 let tool_result = result.unwrap();
2291 assert_eq!(tool_result.tool_name, "read_file");
2292 }
2293
2294 #[test]
2295 fn test_agent_loop_config_token_lifetime_zero_disabled() {
2296 let config = AgentLoopConfig {
2297 max_iterations: 10,
2298 enable_tools: false,
2299 require_approval: false,
2300 prompt_injection_protection: false,
2301 token_lifetime_secs: 0,
2302 no_final_required: false,
2303 fallback_chain: None,
2304 token_budget: None,
2305 ravenfabric: None,
2306 };
2307 assert_eq!(config.token_lifetime_secs, 0);
2308 }
2310
2311 #[test]
2312 fn test_agent_loop_config_token_lifetime_nonzero() {
2313 let config = AgentLoopConfig {
2314 max_iterations: 10,
2315 enable_tools: false,
2316 require_approval: false,
2317 prompt_injection_protection: false,
2318 token_lifetime_secs: 3600,
2319 no_final_required: false,
2320 fallback_chain: None,
2321 token_budget: None,
2322 ravenfabric: None,
2323 };
2324 assert_eq!(config.token_lifetime_secs, 3600);
2325 }
2326
2327 #[test]
2328 fn test_agent_loop_config_default_includes_token_lifetime() {
2329 let config = AgentLoopConfig::default();
2330 assert_eq!(config.token_lifetime_secs, 0);
2331 }
2332}