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