Skip to main content

aagt_core/agent/
core.rs

1//! Agent system - the core AI agent abstraction
2
3use std::sync::Arc;
4use tokio::sync::broadcast;
5use tracing::{info, instrument, error};
6use anyhow;
7
8use crate::error::{Error, Result};
9use crate::agent::context::ContextInjector;
10use crate::agent::message::{Message, Role, Content};
11use crate::agent::provider::Provider;
12use crate::skills::tool::{Tool, ToolSet};
13use crate::agent::streaming::StreamingResponse;
14use crate::skills::tool::memory::{SearchHistoryTool, RememberThisTool, TieredSearchTool, FetchDocumentTool}; // Corrected import for memory tools
15use crate::agent::memory::MemoryManager;
16use crate::agent::context::{ContextManager, ContextConfig}; // ContextInjector is already imported above
17use crate::agent::multi_agent::{Coordinator, AgentRole, MultiAgent, AgentMessage};
18use crate::agent::personality::{Persona, PersonalityManager};
19use crate::agent::scheduler::Scheduler;
20use crate::skills::tool::{DelegateTool, CronTool};
21use crate::infra::notification::{Notifier, NotifyChannel};
22
23/// Configuration for an Agent
24#[derive(Debug, Clone)]
25pub struct AgentConfig {
26    /// Name of the agent (for logging/identity)
27    pub name: String,
28    /// Model to use (provider specific string)
29    pub model: String,
30    /// System prompt / Preamble
31    pub preamble: String,
32    /// Temperature for generation
33    pub temperature: Option<f64>,
34    /// Max tokens to generate
35    pub max_tokens: Option<u64>,
36    /// Additional provider-specific parameters
37    pub extra_params: Option<serde_json::Value>,
38    /// Policy for risky tools
39    pub tool_policy: RiskyToolPolicy,
40    /// Max history messages to send to LLM (Sliding window)
41    pub max_history_messages: usize,
42    /// Max characters allowed in tool output before truncation
43    pub max_tool_output_chars: usize,
44    /// Enable strict JSON mode (response_format: json_object)
45    pub json_mode: bool,
46    /// Optional personality profile
47    pub persona: Option<Persona>,
48    /// Role of the agent in a multi-agent system
49    pub role: AgentRole,
50}
51
52impl Default for AgentConfig {
53    fn default() -> Self {
54        Self {
55            name: "agent".to_string(),
56            model: "gpt-4o".to_string(),
57            preamble: "You are a helpful AI assistant.".to_string(),
58            temperature: Some(0.7),
59            max_tokens: Some(4096),
60            extra_params: None,
61            tool_policy: RiskyToolPolicy::default(),
62            max_history_messages: 20,
63            max_tool_output_chars: 4096,
64            json_mode: false,
65            persona: None,
66            role: AgentRole::Assistant,
67        }
68    }
69}
70
71/// Policy for tool execution
72#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
73#[serde(rename_all = "snake_case")]
74pub enum ToolPolicy {
75    /// Allow execution without approval
76    Auto,
77    /// Require explicit approval
78    RequiresApproval,
79    /// Disable execution completely
80    Disabled,
81}
82
83/// Configuration for risky tool policies
84#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
85pub struct RiskyToolPolicy {
86    /// Default policy for all tools
87    pub default_policy: ToolPolicy,
88    /// Overrides for specific tools
89    pub overrides: std::collections::HashMap<String, ToolPolicy>,
90}
91
92impl Default for RiskyToolPolicy {
93    fn default() -> Self {
94        Self {
95            default_policy: ToolPolicy::Auto,
96            overrides: std::collections::HashMap::new(),
97        }
98    }
99}
100
101/// Events emitted by the Agent during execution
102#[derive(Debug, Clone, serde::Serialize)]
103#[serde(tag = "type", content = "data", rename_all = "snake_case")]
104pub enum AgentEvent {
105    /// Agent started thinking (prompt received)
106    Thinking { prompt: String },
107    /// Agent decided to use a tool
108    ToolCall { tool: String, input: String },
109    /// Tool execution requires approval
110    ApprovalPending { tool: String, input: String },
111    /// Tool execution finished
112    ToolResult { tool: String, output: String },
113    /// Agent generated a final response
114    Response { content: String },
115    /// Error occurred
116    Error { message: String },
117}
118
119/// Handler for user approvals
120#[async_trait::async_trait]
121pub trait ApprovalHandler: Send + Sync {
122    /// Request approval for a tool call
123    async fn approve(&self, tool_name: &str, arguments: &str) -> anyhow::Result<bool>;
124}
125
126/// A default approval handler that rejects all
127pub struct RejectAllApprovalHandler;
128
129#[async_trait::async_trait]
130impl ApprovalHandler for RejectAllApprovalHandler {
131    async fn approve(&self, _tool: &str, _args: &str) -> anyhow::Result<bool> {
132        Ok(false)
133    }
134}
135
136/// Request sent to the channel handler
137#[derive(Debug)]
138pub struct ApprovalRequest {
139    /// Unique ID for this request
140    pub id: String,
141    /// Tool name
142    pub tool_name: String,
143    /// Tool arguments
144    pub arguments: String,
145    /// Responder channel
146    pub responder: tokio::sync::oneshot::Sender<bool>,
147}
148
149/// A handler that sends approval requests via a channel
150pub struct ChannelApprovalHandler {
151    sender: tokio::sync::mpsc::Sender<ApprovalRequest>,
152}
153
154impl ChannelApprovalHandler {
155    /// Create a new channel handler
156    pub fn new(sender: tokio::sync::mpsc::Sender<ApprovalRequest>) -> Self {
157        Self { sender }
158    }
159}
160
161#[async_trait::async_trait]
162impl ApprovalHandler for ChannelApprovalHandler {
163    async fn approve(&self, tool_name: &str, arguments: &str) -> anyhow::Result<bool> {
164        let (tx, rx) = tokio::sync::oneshot::channel();
165        
166        let request = ApprovalRequest {
167            id: uuid::Uuid::new_v4().to_string(),
168            tool_name: tool_name.to_string(),
169            arguments: arguments.to_string(),
170            responder: tx,
171        };
172
173        self.sender.send(request).await
174            .map_err(|_| Error::Internal("Approval channel closed".to_string()))?;
175
176        // Wait for response
177        let approved = rx.await
178            .map_err(|_| Error::Internal("Approval responder dropped".to_string()))?;
179            
180        Ok(approved)
181    }
182}
183
184// use crate::infra::notification::{Notifier, NotifyChannel}; // Already imported at top
185
186/// The main Agent struct
187pub struct Agent<P: Provider> {
188    provider: Arc<P>,
189    tools: ToolSet,
190    config: AgentConfig,
191    context_manager: ContextManager,
192    events: broadcast::Sender<AgentEvent>,
193    approval_handler: Arc<dyn ApprovalHandler>,
194    notifier: Option<Arc<dyn Notifier>>,
195}
196
197impl<P: Provider> Agent<P> {
198    /// Create a new agent builder
199    pub fn builder(provider: P) -> AgentBuilder<P> {
200        AgentBuilder::new(provider)
201    }
202
203    /// Subscribe to agent events
204    pub fn subscribe(&self) -> broadcast::Receiver<AgentEvent> {
205        self.events.subscribe()
206    }
207
208    /// Helper to emit events safely
209    fn emit(&self, event: AgentEvent) {
210        if let Err(e) = self.events.send(event) {
211            tracing::debug!("Failed to emit event (no receivers): {}", e);
212        }
213    }
214    
215    /// Send a notification via the configured notifier
216    pub async fn notify(&self, channel: NotifyChannel, message: &str) -> Result<()> {
217        if let Some(notifier) = &self.notifier {
218             notifier.notify(channel, message).await
219        } else {
220             // If no notifier configured, log warning but don't fail hard
221             tracing::warn!("Agent tried to notify but no notifier is configured: {}", message);
222             Ok(())
223        }
224    }
225
226    /// Send a prompt and get a response (non-streaming)
227    #[instrument(skip(self, prompt), fields(model = %self.config.model))]
228    pub async fn prompt(&self, prompt: impl Into<String>) -> Result<String> {
229        let prompt_str = prompt.into();
230        self.emit(AgentEvent::Thinking { prompt: prompt_str.clone() });
231        
232        let messages = vec![
233            Message::user(prompt_str)
234        ];
235        
236        self.chat(messages).await
237    }
238
239    /// Send messages and get a response (non-streaming)
240    #[instrument(skip(self, messages), fields(model = %self.config.model, message_count = messages.len()))]
241    pub async fn chat(&self, mut messages: Vec<Message>) -> Result<String> {
242        let mut steps = 0;
243        const MAX_STEPS: usize = 15;
244
245        loop {
246            if steps >= MAX_STEPS {
247                return Err(Error::agent_config("Max agent steps exceeded"));
248            }
249            steps += 1;
250
251            if let Some(last) = messages.last() {
252                 if last.role == Role::User {
253                    self.emit(AgentEvent::Thinking { prompt: last.content.as_text() });
254                 }
255            }
256
257            info!("Agent starting chat completion (step {})", steps);
258
259            // Context Window Management via ContextManager
260            let context_messages = self.context_manager.build_context(&messages)
261                .map_err(|e| Error::agent_config(format!("Failed to build context: {}", e)))?;
262
263            let stream = self.stream_chat(context_messages).await?;
264            
265            let mut full_text = String::new();
266            let mut tool_calls = Vec::new(); // (id, name, args)
267
268            let mut stream_inner = stream.into_inner();
269
270            // Consume the stream
271            use futures::StreamExt;
272            while let Some(chunk) = stream_inner.next().await {
273                match chunk? {
274                    crate::agent::streaming::StreamingChoice::Message(text) => {
275                        full_text.push_str(&text);
276                    }
277                    crate::agent::streaming::StreamingChoice::ToolCall { id, name, arguments } => {
278                        tool_calls.push((id, name, arguments));
279                    }
280                     crate::agent::streaming::StreamingChoice::ParallelToolCalls(map) => {
281                         let mut sorted: Vec<_> = map.into_iter().collect();
282                         sorted.sort_by_key(|(k,_)| *k);
283                         for (_, tc) in sorted {
284                             tool_calls.push((tc.id, tc.name, tc.arguments));
285                         }
286                    }
287                    _ => {}
288                }
289            }
290
291            // If no tool calls, we are done
292            if tool_calls.is_empty() {
293                self.emit(AgentEvent::Response { content: full_text.clone() });
294                return Ok(full_text);
295            }
296
297            // We have tool calls.
298            // 1. Append Assistant Message (Thought + Calls) to history
299            let mut parts = Vec::new();
300            if !full_text.is_empty() {
301                parts.push(crate::agent::message::ContentPart::Text { text: full_text.clone() });
302            }
303            for (id, name, args) in &tool_calls {
304                parts.push(crate::agent::message::ContentPart::ToolCall {
305                    id: id.clone(),
306                    name: name.clone(),
307                    arguments: args.clone(),
308                });
309            }
310            messages.push(Message {
311                role: Role::Assistant,
312                name: None,
313                content: Content::Parts(parts),
314            });
315
316            // 2. Execute Tools (Parallel)
317            let tools = &self.tools;
318            let policy = &self.config.tool_policy;
319            let events = &self.events;
320            let approval_handler = &self.approval_handler;
321            
322            let mut futures = Vec::new();
323            
324            for (id, name, args) in tool_calls {
325                let name_clone = name.clone();
326                let id_clone = id.clone();
327                let args_str = args.to_string();
328                
329                futures.push(async move {
330                    // Check policy
331                    let effective_policy = policy.overrides.get(&name_clone)
332                        .unwrap_or(&policy.default_policy);
333                    
334                    let result = match effective_policy {
335                        ToolPolicy::Disabled => {
336                            Err(Error::tool_execution(name_clone.clone(), "Tool execution is disabled by policy".to_string()))
337                        }
338                        ToolPolicy::RequiresApproval => {
339                            let _ = events.send(AgentEvent::ApprovalPending { 
340                                tool: name_clone.clone(), 
341                                input: args_str.clone() 
342                            });
343                            
344                            // Ask approval handler
345                            match approval_handler.approve(&name_clone, &args_str).await {
346                                Ok(true) => {
347                                    // Approved! Proceed to auto
348                                    let _ = events.send(AgentEvent::ToolCall { 
349                                        tool: name_clone.clone(), 
350                                        input: args_str.clone() 
351                                    });
352                                    tools.call(&name_clone, &args_str).await
353                                        .map_err(|e| Error::tool_execution(name_clone.clone(), e.to_string()))
354                                }
355                                Ok(false) => {
356                                    Err(Error::ToolApprovalRequired { tool_name: name_clone.clone() })
357                                }
358                                Err(e) => {
359                                    Err(Error::tool_execution(name_clone.clone(), format!("Approval check failed: {}", e)))
360                                }
361                            }
362                        }
363                        ToolPolicy::Auto => {
364                            let _ = events.send(AgentEvent::ToolCall { 
365                                tool: name_clone.clone(), 
366                                input: args_str.clone() 
367                            });
368                            // internal tool call returns anyhow::Result
369                            tools.call(&name_clone, &args_str).await
370                                .map_err(|e| Error::tool_execution(name_clone.clone(), e.to_string()))
371                        }
372                    };
373                    
374                    let output = match result {
375                        Ok(s) => {
376                            let _ = events.send(AgentEvent::ToolResult { 
377                                tool: name_clone.clone(), 
378                                output: s.clone() 
379                            });
380                            s
381                        }
382                        Err(e) => {
383                            let _ = events.send(AgentEvent::Error { message: e.to_string() });
384                            format!("Error: {}", e)
385                        }
386                    };
387
388                    (id_clone, output, name_clone)
389                });
390            }
391            
392            let results = futures::future::join_all(futures).await;
393
394            // 3. Append Tool Results to history
395            for (id, output, name) in results {
396                 messages.push(Message {
397                    role: Role::Tool,
398                    name: None,
399                    content: Content::Parts(vec![crate::agent::message::ContentPart::ToolResult {
400                        tool_call_id: id,
401                        content: output,
402                        name: Some(name),
403                    }]),
404                });
405            }
406        }
407    }
408
409    /// Stream a prompt response
410    pub async fn stream(&self, prompt: impl Into<String>) -> Result<StreamingResponse> {
411        let messages = vec![Message::user(prompt.into())];
412        self.stream_chat(messages).await
413    }
414
415    /// Stream a chat response
416    pub async fn stream_chat(&self, messages: Vec<Message>) -> Result<StreamingResponse> {
417        let mut extra = self.config.extra_params.clone().unwrap_or(serde_json::Value::Object(serde_json::Map::new()));
418        
419        // Inject JSON mode if enabled
420        if self.config.json_mode {
421            if let serde_json::Value::Object(ref mut map) = extra {
422                if !map.contains_key("response_format") {
423                     map.insert("response_format".to_string(), serde_json::json!({ "type": "json_object" }));
424                }
425            }
426        }
427
428        self.provider
429            .stream_completion(
430                &self.config.model,
431                Some(&self.config.preamble), // Use preamble as system prompt
432                messages,
433                self.tools.definitions().await,
434                self.config.temperature,
435                self.config.max_tokens,
436                Some(extra),
437            )
438            .await
439    }
440
441    /// Call a tool by name (Direct call helper)
442    #[instrument(skip(self, arguments), fields(tool_name = %name))]
443    pub async fn call_tool(&self, name: &str, arguments: &str) -> Result<String> {
444        // 1. Check Policy
445        let policy = self.config.tool_policy.overrides.get(name)
446            .unwrap_or(&self.config.tool_policy.default_policy);
447
448        match policy {
449            ToolPolicy::Disabled => {
450                 return Err(Error::tool_execution(name.to_string(), "Tool execution is disabled by policy".to_string()));
451            }
452            ToolPolicy::RequiresApproval => {
453                self.emit(AgentEvent::ApprovalPending { tool: name.to_string(), input: arguments.to_string() });
454                
455                match self.approval_handler.approve(name, arguments).await {
456                    Ok(true) => {}, // Proceed
457                    Ok(false) => return Err(Error::ToolApprovalRequired { tool_name: name.to_string() }),
458                    Err(e) => return Err(Error::tool_execution(name.to_string(), format!("Approval check failed: {}", e)))
459                }
460            }
461            ToolPolicy::Auto => {} // Proceed
462        }
463
464        self.emit(AgentEvent::ToolCall { tool: name.to_string(), input: arguments.to_string() });
465
466        let result = self.tools.call(name, arguments).await;
467        
468        match result {
469            Ok(mut output) => {
470                // Quota Protection: Truncate tool output if too long
471                if output.len() > self.config.max_tool_output_chars {
472                    let original_len = output.len();
473                    output.truncate(self.config.max_tool_output_chars);
474                    output.push_str(&format!("\n\n(Note: Output truncated from {} to {} chars to save tokens)", 
475                        original_len, self.config.max_tool_output_chars));
476                }
477
478                self.emit(AgentEvent::ToolResult { tool: name.to_string(), output: output.clone() });
479                Ok(output)
480            },
481            Err(e) => {
482                self.emit(AgentEvent::Error { message: e.to_string() });
483                // Map anyhow error to ToolExecution error
484                Err(Error::tool_execution(name.to_string(), e.to_string()))
485            }
486        }
487    }
488
489    /// Check if agent has a tool
490    pub fn has_tool(&self, name: &str) -> bool {
491        self.tools.contains(name)
492    }
493
494    /// Add tool definitions
495    pub async fn tool_definitions(&self) -> Vec<crate::skills::tool::ToolDefinition> {
496        self.tools.definitions().await
497    }
498
499    /// Get the agent's configuration
500    pub fn config(&self) -> &AgentConfig {
501        &self.config
502    }
503
504    /// Get the model name
505    pub fn model(&self) -> &str {
506        &self.config.model
507    }
508
509    /// Start a proactive loop that listens for tasks from multiple sources
510    pub async fn listen(
511        &self, 
512        mut user_input: tokio::sync::mpsc::Receiver<String>,
513        mut external_events: tokio::sync::mpsc::Receiver<AgentMessage>
514    ) -> Result<()> {
515        info!("Agent {} starting proactive loop", self.config.name);
516        
517        loop {
518            tokio::select! {
519                // 1. Handle user input
520                input = user_input.recv() => {
521                    match input {
522                        Some(text) => {
523                            if let Err(e) = self.process(&text).await {
524                                error!("Error in proactive user task: {}", e);
525                            }
526                        }
527                        None => {
528                            info!("User input channel closed, exiting proactive loop");
529                            break;
530                        }
531                    }
532                }
533                
534                // 2. Handle external agent/system messages (e.g. from Scheduler)
535                msg = external_events.recv() => {
536                    match msg {
537                        Some(message) => {
538                            if let Err(e) = self.handle_message(message).await {
539                                error!("Error in proactive external task: {}", e);
540                            }
541                        }
542                        None => {
543                            info!("External events channel closed, exiting proactive loop");
544                            break;
545                        }
546                    }
547                }
548            }
549        }
550        
551        Ok(())
552    }
553}
554
555/// Builder for creating agents
556pub struct AgentBuilder<P: Provider> {
557    provider: P,
558    tools: ToolSet,
559    config: AgentConfig,
560    injectors: Vec<Box<dyn ContextInjector>>,
561    approval_handler: Option<Arc<dyn ApprovalHandler>>,
562    notifier: Option<Arc<dyn Notifier>>,
563    /// Security: Track if Python Sidecar is enabled (mutually exclusive with DynamicSkill)
564    has_sidecar: bool,
565    /// Security: Track if DynamicSkill is enabled (mutually exclusive with Sidecar)
566    has_dynamic_skill: bool,
567}
568
569impl<P: Provider> AgentBuilder<P> {
570    /// Create a new builder with a provider
571    pub fn new(provider: P) -> Self {
572        Self {
573            provider,
574            tools: ToolSet::new(),
575            config: AgentConfig::default(),
576            injectors: Vec::new(),
577            approval_handler: None,
578            notifier: None,
579            has_sidecar: false,
580            has_dynamic_skill: false,
581        }
582    }
583
584    /// Set the model to use
585    pub fn model(mut self, model: impl Into<String>) -> Self {
586        self.config.model = model.into();
587        self
588    }
589
590    /// Set the system prompt
591    pub fn system_prompt(mut self, prompt: impl Into<String>) -> Self {
592        self.config.preamble = prompt.into();
593        self
594    }
595
596    /// Alias for system_prompt
597    pub fn preamble(self, prompt: impl Into<String>) -> Self {
598        self.system_prompt(prompt)
599    }
600
601    /// Set the temperature
602    pub fn temperature(mut self, temp: f64) -> Self {
603        self.config.temperature = Some(temp);
604        self
605    }
606
607    /// Set max tokens
608    pub fn max_tokens(mut self, tokens: u64) -> Self {
609        self.config.max_tokens = Some(tokens);
610        self
611    }
612
613    /// Add extra provider-specific parameters
614    pub fn extra_params(mut self, params: serde_json::Value) -> Self {
615        self.config.extra_params = Some(params);
616        self
617    }
618
619    /// Set tool policy
620    pub fn tool_policy(mut self, policy: RiskyToolPolicy) -> Self {
621        self.config.tool_policy = policy;
622        self
623    }
624
625    /// Set external approval handler
626    pub fn approval_handler(mut self, handler: impl ApprovalHandler + 'static) -> Self {
627        self.approval_handler = Some(Arc::new(handler));
628        self
629    }
630
631    /// Set max history messages (sliding window)
632    pub fn max_history_messages(mut self, count: usize) -> Self {
633        self.config.max_history_messages = count;
634        self
635    }
636
637    /// Set max tool output characters
638    pub fn max_tool_output_chars(mut self, count: usize) -> Self {
639        self.config.max_tool_output_chars = count;
640        self
641    }
642
643    /// Enable strict JSON mode (enforces response_format: json_object)
644    pub fn json_mode(mut self, enable: bool) -> Self {
645        self.config.json_mode = enable;
646        self
647    }
648    
649    /// Set the agent's personality
650    pub fn persona(mut self, persona: Persona) -> Self {
651        self.config.persona = Some(persona);
652        self
653    }
654    
655    /// Set a notifier
656    pub fn notifier(mut self, notifier: impl Notifier + 'static) -> Self {
657        self.notifier = Some(Arc::new(notifier));
658        self
659    }
660
661    /// Set the agent's role
662    pub fn role(mut self, role: AgentRole) -> Self {
663        self.config.role = role;
664        self
665    }
666
667    /// Add a context injector
668    pub fn context_injector(mut self, injector: impl ContextInjector + 'static) -> Self {
669        self.injectors.push(Box::new(injector));
670        self
671    }
672
673    /// Add a tool
674    pub fn tool<T: Tool + 'static>(mut self, tool: T) -> Self {
675        self.tools.add(tool);
676        self
677    }
678
679    /// Add a shared tool
680    pub fn shared_tool(mut self, tool: Arc<dyn Tool>) -> Self {
681        self.tools.add_shared(tool);
682        self
683    }
684
685    /// Add multiple tools from a toolset
686    pub fn tools(mut self, tools: ToolSet) -> Self {
687        for (_, tool) in tools.iter() {
688            self.tools.add_shared(Arc::clone(tool));
689        }
690        self
691    }
692
693    /// Add memory tools using the provided memory manager
694    pub fn with_memory(mut self, memory: Arc<MemoryManager>) -> Self {
695        // Use the unified memory (MemoryManager implements Memory) for tools
696        // to ensure Union Search is available.
697        self.tools.add(SearchHistoryTool::new(memory.clone()));
698        self.tools.add(RememberThisTool::new(memory.clone()));
699        self.tools.add(TieredSearchTool::new(memory.clone()));
700        self.tools.add(FetchDocumentTool::new(memory));
701        
702        self
703    }
704
705    /// Add DynamicSkill support (ClawHub skills, custom scripts)
706    /// 
707    /// # Security
708    /// 
709    /// **CRITICAL**: DynamicSkill and Python Sidecar are mutually exclusive.
710    /// This method will return an error if Python Sidecar has already been configured.
711    /// 
712    /// **Rationale**: If both are enabled, malicious DynamicSkills can pollute the
713    /// Agent's context with secrets, which may then be used by LLM-generated Python
714    /// code in the unsandboxed Sidecar to exfiltrate data.
715    /// 
716    /// See SECURITY.md for details.
717    pub fn with_dynamic_skills(mut self, skill_loader: Arc<crate::skills::SkillLoader>) -> Result<Self> {
718        // Security check: prevent enabling both Sidecar and DynamicSkill
719        if self.has_sidecar {
720            return Err(Error::agent_config(
721                "Security Error: Cannot enable DynamicSkill when Python Sidecar is configured. \
722                These are mutually exclusive due to context pollution risks. \
723                See SECURITY.md for details."
724            ));
725        }
726        
727        // Add all loaded skills as tools
728        for skill_ref in skill_loader.skills.iter() {
729            self.tools.add_shared(Arc::clone(skill_ref.value()) as Arc<dyn crate::skills::tool::Tool>);
730        }
731        
732        // Add ClawHub and ReadSkillDoc tools
733        self.tools.add(crate::skills::ClawHubTool::new(Arc::clone(&skill_loader)));
734        self.tools.add(crate::skills::ReadSkillDoc::new(skill_loader));
735        
736        self.has_dynamic_skill = true;
737        
738        Ok(self)
739    }
740
741    /// Add code interpreter capability using the given sidecar address
742    /// 
743    /// # Security
744    /// 
745    /// **CRITICAL**: Python Sidecar and DynamicSkill are mutually exclusive.
746    /// This method will return an error if DynamicSkill has already been configured.
747    /// 
748    /// **Rationale**: Python Sidecar has no sandbox isolation. If DynamicSkill is also
749    /// enabled, malicious skills can pollute the Agent's context, leading to secret
750    /// exfiltration via LLM-generated Python code in the Sidecar.
751    /// 
752    /// See SECURITY.md for details.
753    pub async fn with_code_interpreter(mut self, address: impl Into<String>) -> Result<Self> {
754        // Security check: prevent enabling both Sidecar and DynamicSkill
755        if self.has_dynamic_skill {
756            return Err(Error::agent_config(
757                "Security Error: Cannot enable Python Sidecar when DynamicSkill is configured. \
758                These are mutually exclusive due to context pollution risks. \
759                See SECURITY.md for details."
760            ));
761        }
762        
763        let sidecar = crate::skills::capabilities::Sidecar::connect(address.into()).await?;
764        let shared_sidecar = Arc::new(tokio::sync::Mutex::new(sidecar));
765        
766        self.tools.add(crate::skills::tool::code_interpreter::CodeInterpreter::new(shared_sidecar));
767        self.has_sidecar = true;
768        
769        Ok(self)
770    }
771
772    /// Build the agent
773    /// 
774    /// # Security Defaults
775    /// 
776    /// If neither Python Sidecar nor DynamicSkill has been explicitly configured,
777    /// this method will automatically enable DynamicSkill with default settings:
778    /// - Skills directory: `./skills`
779    /// - Network access: disabled (secure sandbox)
780    /// 
781    /// To use Python Sidecar instead, call `.with_code_interpreter()` before `.build()`.
782    pub fn build(mut self) -> Result<Agent<P>> {
783        // Validate configuration
784        if self.config.model.is_empty() {
785            return Err(Error::agent_config("model name cannot be empty"));
786        }
787        if self.config.max_history_messages == 0 {
788            return Err(Error::agent_config("max_history_messages must be at least 1"));
789        }
790
791        // SECURITY DEFAULT: Auto-enable DynamicSkill if no execution model configured
792        if !self.has_sidecar && !self.has_dynamic_skill {
793            info!("No execution model configured. Auto-enabling DynamicSkill (default)...");
794            
795            // Try to load skills from default directory
796            let skill_loader = Arc::new(crate::skills::SkillLoader::new("./skills"));
797            
798            // Attempt to load skills (non-fatal if directory doesn't exist)
799            match tokio::task::block_in_place(|| {
800                tokio::runtime::Handle::current().block_on(skill_loader.load_all())
801            }) {
802                Ok(_) => {
803                    info!("Loaded DynamicSkills from ./skills");
804                    
805                    // Add all loaded skills as tools
806                    for skill_ref in skill_loader.skills.iter() {
807                        self.tools.add_shared(Arc::clone(skill_ref.value()) as Arc<dyn crate::skills::tool::Tool>);
808                    }
809                    
810                    // Add ClawHub and ReadSkillDoc tools
811                    self.tools.add(crate::skills::ClawHubTool::new(Arc::clone(&skill_loader)));
812                    self.tools.add(crate::skills::ReadSkillDoc::new(skill_loader));
813                    
814                    self.has_dynamic_skill = true;
815                },
816                Err(e) => {
817                    // Non-fatal: Skills directory doesn't exist or is empty
818                    info!("DynamicSkill auto-enable skipped (no skills found): {}", e);
819                    // Continue without skills - agent will still function with other tools
820                }
821            }
822        }
823
824        let (tx, _) = broadcast::channel(1000);
825
826        let mut context_config = ContextConfig::default();
827        context_config.max_history_messages = self.config.max_history_messages;
828        if let Some(tokens) = self.config.max_tokens {
829            // Rough heuristic: Context window is usually larger than max_tokens (generation limit)
830            // But we don't have model context window size in config yet.
831            // For now, let's just ensure we respect max_history_messages primarily.
832            context_config.response_reserve = tokens as usize;
833        }
834
835        let mut context_manager = ContextManager::new(context_config);
836        context_manager.set_system_prompt(self.config.preamble.clone());
837        
838        // Inject all tools as TS interfaces in the system prompt
839        // This fulfills the 'Replace JSON with TS in Prompt' requirement.
840        context_manager.add_injector(Box::new(self.tools.clone()));
841
842        for injector in self.injectors {
843            context_manager.add_injector(injector);
844        }
845
846        // Auto-inject personality if configured
847        if let Some(persona) = &self.config.persona {
848            context_manager.add_injector(Box::new(PersonalityManager::new(persona.clone())));
849        }
850
851        Ok(Agent {
852            provider: Arc::new(self.provider),
853            tools: self.tools,
854            config: self.config,
855            context_manager,
856            events: tx,
857            approval_handler: self.approval_handler.unwrap_or_else(|| Arc::new(RejectAllApprovalHandler)),
858            notifier: self.notifier,
859        })
860    }
861
862    /// Add delegation support using the provided coordinator
863    pub fn with_delegation(mut self, coordinator: Arc<Coordinator>) -> Self {
864        self.tools.add(DelegateTool::new(Arc::downgrade(&coordinator)));
865        self
866    }
867
868    /// Add scheduling support using the provided scheduler
869    pub fn with_scheduler(mut self, scheduler: Arc<Scheduler>) -> Self {
870        self.tools.add(CronTool::new(Arc::downgrade(&scheduler)));
871        self
872    }
873}
874
875#[async_trait::async_trait]
876impl<P: Provider> MultiAgent for Agent<P> {
877    fn role(&self) -> AgentRole {
878        self.config.role.clone()
879    }
880
881    async fn handle_message(&self, message: AgentMessage) -> Result<Option<AgentMessage>> {
882        info!("Agent {:?} handling message from {:?}", self.role(), message.from);
883        let response = self.prompt(message.content).await?;
884        
885        Ok(Some(AgentMessage {
886            from: self.role(),
887            to: Some(message.from),
888            content: response,
889            msg_type: crate::agent::multi_agent::MessageType::Response,
890        }))
891    }
892
893    async fn process(&self, input: &str) -> Result<String> {
894        self.prompt(input).await
895    }
896}
897
898#[cfg(test)]
899mod tests {
900    use super::*;
901
902    #[test]
903    fn test_agent_config_default() {
904        let config = AgentConfig::default();
905        assert_eq!(config.model, "gpt-4o");
906        assert_eq!(config.max_tokens, Some(4096));
907    }
908}