helios_engine/
agent.rs

1//! # Agent Module
2//!
3//! This module defines the `Agent` struct, the core of the Helios Engine. Agents are
4//! autonomous entities that can interact with users, use tools, and manage their
5//! own chat history. The `AgentBuilder` provides a convenient way to construct
6//! and configure agents.
7
8#![allow(dead_code)]
9#![allow(unused_variables)]
10use crate::chat::{ChatMessage, ChatSession};
11use crate::config::Config;
12use crate::error::{HeliosError, Result};
13use crate::llm::{LLMClient, LLMProviderType};
14use crate::tools::{ToolRegistry, ToolResult};
15use serde_json::Value;
16
17/// Prefix for agent-specific keys in the chat session metadata.
18const AGENT_MEMORY_PREFIX: &str = "agent:";
19
20/// Represents an LLM-powered agent that can chat, use tools, and manage a conversation.
21pub struct Agent {
22    /// The name of the agent.
23    name: String,
24    /// The client for interacting with the Large Language Model.
25    llm_client: LLMClient,
26    /// The registry of tools available to the agent.
27    tool_registry: ToolRegistry,
28    /// The chat session, which stores the conversation history.
29    chat_session: ChatSession,
30    /// The maximum number of iterations for tool execution in a single turn.
31    max_iterations: usize,
32    /// Whether the agent uses ReAct mode (Reasoning and Acting).
33    react_mode: bool,
34    /// Custom reasoning prompt for ReAct mode.
35    react_prompt: Option<String>,
36}
37
38impl Agent {
39    /// Creates a new agent with the given name and configuration.
40    ///
41    /// # Arguments
42    ///
43    /// * `name` - The name of the agent.
44    /// * `config` - The configuration for the agent.
45    ///
46    /// # Returns
47    ///
48    /// A `Result` containing the new `Agent` instance.
49    async fn new(name: impl Into<String>, config: Config) -> Result<Self> {
50        // Priority: Candle > Local > Remote (API)
51
52        #[cfg(feature = "candle")]
53        let provider_type = if let Some(candle_config) = config.candle {
54            LLMProviderType::Candle(candle_config)
55        } else {
56            #[cfg(feature = "local")]
57            {
58                if let Some(local_config) = config.local {
59                    LLMProviderType::Local(local_config)
60                } else {
61                    LLMProviderType::Remote(config.llm)
62                }
63            }
64            #[cfg(not(feature = "local"))]
65            {
66                LLMProviderType::Remote(config.llm)
67            }
68        };
69
70        #[cfg(all(feature = "local", not(feature = "candle")))]
71        let provider_type = if let Some(local_config) = config.local {
72            LLMProviderType::Local(local_config)
73        } else {
74            LLMProviderType::Remote(config.llm)
75        };
76
77        #[cfg(not(any(feature = "local", feature = "candle")))]
78        let provider_type = LLMProviderType::Remote(config.llm);
79
80        let llm_client = LLMClient::new(provider_type).await?;
81
82        Ok(Self {
83            name: name.into(),
84            llm_client,
85            tool_registry: ToolRegistry::new(),
86            chat_session: ChatSession::new(),
87            max_iterations: 10,
88            react_mode: false,
89            react_prompt: None,
90        })
91    }
92
93    /// Returns a new `AgentBuilder` for constructing an agent.
94    ///
95    /// # Arguments
96    ///
97    /// * `name` - The name of the agent.
98    pub fn builder(name: impl Into<String>) -> AgentBuilder {
99        AgentBuilder::new(name)
100    }
101
102    /// Creates a quick-start agent with minimal configuration.
103    ///
104    /// This is the simplest way to create an agent - just provide a name and it uses
105    /// the default configuration (reads from config.toml if available, otherwise uses defaults).
106    ///
107    /// # Arguments
108    ///
109    /// * `name` - The name of the agent.
110    ///
111    /// # Example
112    ///
113    /// ```rust,no_run
114    /// # use helios_engine::Agent;
115    /// # async fn example() -> helios_engine::Result<()> {
116    /// let mut agent = Agent::quick("MyAgent").await?;
117    /// let response = agent.chat("Hello!").await?;
118    /// println!("{}", response);
119    /// # Ok(())
120    /// # }
121    /// ```
122    pub async fn quick(name: impl Into<String>) -> Result<Self> {
123        let config = Config::load_or_default("config.toml");
124        Agent::builder(name).config(config).build().await
125    }
126
127    /// Returns the name of the agent.
128    pub fn name(&self) -> &str {
129        &self.name
130    }
131
132    /// Sets the system prompt for the agent.
133    ///
134    /// # Arguments
135    ///
136    /// * `prompt` - The system prompt to set.
137    pub fn set_system_prompt(&mut self, prompt: impl Into<String>) {
138        self.chat_session = self.chat_session.clone().with_system_prompt(prompt);
139    }
140
141    /// Registers a tool with the agent.
142    ///
143    /// # Arguments
144    ///
145    /// * `tool` - The tool to register.
146    pub fn register_tool(&mut self, tool: Box<dyn crate::tools::Tool>) {
147        self.tool_registry.register(tool);
148    }
149
150    /// Returns a reference to the agent's tool registry.
151    pub fn tool_registry(&self) -> &ToolRegistry {
152        &self.tool_registry
153    }
154
155    /// Returns a mutable reference to the agent's tool registry.
156    pub fn tool_registry_mut(&mut self) -> &mut ToolRegistry {
157        &mut self.tool_registry
158    }
159
160    /// Returns a reference to the agent's chat session.
161    pub fn chat_session(&self) -> &ChatSession {
162        &self.chat_session
163    }
164
165    /// Returns a mutable reference to the agent's chat session.
166    pub fn chat_session_mut(&mut self) -> &mut ChatSession {
167        &mut self.chat_session
168    }
169
170    /// Clears the agent's chat history.
171    pub fn clear_history(&mut self) {
172        self.chat_session.clear();
173    }
174
175    /// Sends a message to the agent and gets a response.
176    ///
177    /// # Arguments
178    ///
179    /// * `message` - The message to send.
180    ///
181    /// # Returns
182    ///
183    /// A `Result` containing the agent's response.
184    pub async fn send_message(&mut self, message: impl Into<String>) -> Result<String> {
185        let user_message = message.into();
186        self.chat_session.add_user_message(user_message.clone());
187
188        // Execute agent loop with tool calling
189        let response = self.execute_with_tools().await?;
190
191        Ok(response)
192    }
193
194    /// Default reasoning prompt for ReAct mode.
195    const DEFAULT_REASONING_PROMPT: &'static str = r#"Before taking any action, think through this step by step:
196
1971. What is the user asking for?
1982. What information or tools do I need to answer this?
1993. What is my plan to solve this problem?
200
201Provide your reasoning in a clear, structured way."#;
202
203    /// Generates reasoning for the current task in ReAct mode.
204    ///
205    /// This is a pure function that only generates and returns the reasoning.
206    /// It does not modify the agent's state (chat history).
207    /// The caller is responsible for displaying and storing the reasoning.
208    async fn generate_reasoning(&self) -> Result<String> {
209        let reasoning_prompt = self
210            .react_prompt
211            .as_deref()
212            .unwrap_or(Self::DEFAULT_REASONING_PROMPT);
213
214        // Create a temporary reasoning message
215        let mut reasoning_messages = self.chat_session.get_messages();
216        reasoning_messages.push(ChatMessage::user(reasoning_prompt));
217
218        // Get reasoning from LLM without tools
219        let response = self
220            .llm_client
221            .chat(reasoning_messages, None, None, None, None)
222            .await?;
223
224        Ok(response.content)
225    }
226
227    /// Handles ReAct reasoning if enabled.
228    ///
229    /// This helper method generates reasoning, displays it to the user,
230    /// and adds it to the chat history as an assistant message.
231    /// It should be called at the beginning of tool execution methods.
232    async fn handle_react_reasoning(&mut self) -> Result<()> {
233        // If ReAct mode is enabled, generate reasoning first
234        if self.react_mode && !self.tool_registry.get_definitions().is_empty() {
235            let reasoning = self.generate_reasoning().await?;
236
237            // Display reasoning to user
238            println!("\n💭 ReAct Reasoning:\n{}\n", reasoning);
239
240            // Add reasoning to chat history as an assistant message (not user)
241            // This represents the agent's internal thought process
242            self.chat_session
243                .add_message(ChatMessage::assistant(format!(
244                    "[Reasoning]: {}",
245                    reasoning
246                )));
247        }
248        Ok(())
249    }
250
251    /// Executes the agent's main loop, including tool calls.
252    async fn execute_with_tools(&mut self) -> Result<String> {
253        self.execute_with_tools_streaming().await
254    }
255
256    /// Executes the agent's main loop with streaming, including tool calls.
257    async fn execute_with_tools_streaming(&mut self) -> Result<String> {
258        self.execute_with_tools_streaming_with_params(None, None, None)
259            .await
260    }
261
262    /// Executes the agent's main loop with parameters, including tool calls.
263    async fn execute_with_tools_with_params(
264        &mut self,
265        temperature: Option<f32>,
266        max_tokens: Option<u32>,
267        stop: Option<Vec<String>>,
268    ) -> Result<String> {
269        // Handle ReAct reasoning if enabled
270        self.handle_react_reasoning().await?;
271
272        let mut iterations = 0;
273        let tool_definitions = self.tool_registry.get_definitions();
274
275        loop {
276            if iterations >= self.max_iterations {
277                return Err(HeliosError::AgentError(
278                    "Maximum iterations reached".to_string(),
279                ));
280            }
281
282            let messages = self.chat_session.get_messages();
283            let tools_option = if tool_definitions.is_empty() {
284                None
285            } else {
286                Some(tool_definitions.clone())
287            };
288
289            let response = self
290                .llm_client
291                .chat(
292                    messages,
293                    tools_option,
294                    temperature,
295                    max_tokens,
296                    stop.clone(),
297                )
298                .await?;
299
300            // Check if the response includes tool calls
301            if let Some(ref tool_calls) = response.tool_calls {
302                // Add assistant message with tool calls
303                self.chat_session.add_message(response.clone());
304
305                // Execute each tool call
306                for tool_call in tool_calls {
307                    let tool_name = &tool_call.function.name;
308                    let tool_args: Value = serde_json::from_str(&tool_call.function.arguments)
309                        .unwrap_or(Value::Object(serde_json::Map::new()));
310
311                    let tool_result = self
312                        .tool_registry
313                        .execute(tool_name, tool_args)
314                        .await
315                        .unwrap_or_else(|e| {
316                            ToolResult::error(format!("Tool execution failed: {}", e))
317                        });
318
319                    // Add tool result message
320                    let tool_message = ChatMessage::tool(tool_result.output, tool_call.id.clone());
321                    self.chat_session.add_message(tool_message);
322                }
323
324                iterations += 1;
325                continue;
326            }
327
328            // No tool calls, we have the final response
329            self.chat_session.add_message(response.clone());
330            return Ok(response.content);
331        }
332    }
333
334    /// Executes the agent's main loop with parameters and streaming, including tool calls.
335    async fn execute_with_tools_streaming_with_params(
336        &mut self,
337        temperature: Option<f32>,
338        max_tokens: Option<u32>,
339        stop: Option<Vec<String>>,
340    ) -> Result<String> {
341        // Handle ReAct reasoning if enabled
342        self.handle_react_reasoning().await?;
343
344        let mut iterations = 0;
345        let tool_definitions = self.tool_registry.get_definitions();
346
347        loop {
348            if iterations >= self.max_iterations {
349                return Err(HeliosError::AgentError(
350                    "Maximum iterations reached".to_string(),
351                ));
352            }
353
354            let messages = self.chat_session.get_messages();
355            let tools_option = if tool_definitions.is_empty() {
356                None
357            } else {
358                Some(tool_definitions.clone())
359            };
360
361            let mut streamed_content = String::new();
362
363            let stream_result = self
364                .llm_client
365                .chat_stream(
366                    messages,
367                    tools_option, // Enable tools for streaming
368                    temperature,
369                    max_tokens,
370                    stop.clone(),
371                    |chunk| {
372                        // Print chunk to stdout for visible streaming
373                        print!("{}", chunk);
374                        let _ = std::io::Write::flush(&mut std::io::stdout());
375                        streamed_content.push_str(chunk);
376                    },
377                )
378                .await;
379
380            let response = stream_result?;
381
382            // Print newline after streaming completes
383            println!();
384
385            // Check if the response includes tool calls
386            if let Some(ref tool_calls) = response.tool_calls {
387                // Add assistant message with tool calls
388                let mut msg_with_content = response.clone();
389                msg_with_content.content = streamed_content.clone();
390                self.chat_session.add_message(msg_with_content);
391
392                // Execute each tool call
393                for tool_call in tool_calls {
394                    let tool_name = &tool_call.function.name;
395                    let tool_args: Value = serde_json::from_str(&tool_call.function.arguments)
396                        .unwrap_or(Value::Object(serde_json::Map::new()));
397
398                    let tool_result = self
399                        .tool_registry
400                        .execute(tool_name, tool_args)
401                        .await
402                        .unwrap_or_else(|e| {
403                            ToolResult::error(format!("Tool execution failed: {}", e))
404                        });
405
406                    // Add tool result message
407                    let tool_message = ChatMessage::tool(tool_result.output, tool_call.id.clone());
408                    self.chat_session.add_message(tool_message);
409                }
410
411                iterations += 1;
412                continue;
413            }
414
415            // No tool calls, we have the final response with streamed content
416            let mut final_msg = response;
417            final_msg.content = streamed_content.clone();
418            self.chat_session.add_message(final_msg);
419            return Ok(streamed_content);
420        }
421    }
422
423    /// A convenience method for sending a message to the agent.
424    pub async fn chat(&mut self, message: impl Into<String>) -> Result<String> {
425        self.send_message(message).await
426    }
427
428    /// Ultra-simple alias for chat - just ask a question!
429    pub async fn ask(&mut self, question: impl Into<String>) -> Result<String> {
430        self.chat(question).await
431    }
432
433    /// Sets system prompt and returns self for chaining
434    pub fn with_system_prompt(mut self, prompt: impl Into<String>) -> Self {
435        self.set_system_prompt(prompt);
436        self
437    }
438
439    /// Registers a tool and returns self for chaining
440    pub fn with_tool(mut self, tool: Box<dyn crate::tools::Tool>) -> Self {
441        self.register_tool(tool);
442        self
443    }
444
445    /// Registers multiple tools and returns self for chaining
446    pub fn with_tools(mut self, tools: Vec<Box<dyn crate::tools::Tool>>) -> Self {
447        for tool in tools {
448            self.register_tool(tool);
449        }
450        self
451    }
452
453    /// Sets the maximum number of iterations for tool execution.
454    ///
455    /// # Arguments
456    ///
457    /// * `max` - The maximum number of iterations.
458    pub fn set_max_iterations(&mut self, max: usize) {
459        self.max_iterations = max;
460    }
461
462    /// Returns a summary of the current chat session.
463    pub fn get_session_summary(&self) -> String {
464        self.chat_session.get_summary()
465    }
466
467    /// Clears the agent's memory (agent-scoped metadata).
468    pub fn clear_memory(&mut self) {
469        // Only clear agent-scoped memory keys to avoid wiping general session metadata
470        self.chat_session
471            .metadata
472            .retain(|k, _| !k.starts_with(AGENT_MEMORY_PREFIX));
473    }
474
475    /// Prefixes a key with the agent memory prefix.
476    #[inline]
477    fn prefixed_key(key: &str) -> String {
478        format!("{}{}", AGENT_MEMORY_PREFIX, key)
479    }
480
481    // Agent-scoped memory API (namespaced under "agent:")
482    /// Sets a value in the agent's memory.
483    pub fn set_memory(&mut self, key: impl Into<String>, value: impl Into<String>) {
484        let key = key.into();
485        self.chat_session
486            .set_metadata(Self::prefixed_key(&key), value);
487    }
488
489    /// Gets a value from the agent's memory.
490    pub fn get_memory(&self, key: &str) -> Option<&String> {
491        self.chat_session.get_metadata(&Self::prefixed_key(key))
492    }
493
494    /// Removes a value from the agent's memory.
495    pub fn remove_memory(&mut self, key: &str) -> Option<String> {
496        self.chat_session.remove_metadata(&Self::prefixed_key(key))
497    }
498
499    // Convenience helpers to reduce duplication in examples
500    /// Increments a counter in the agent's memory.
501    pub fn increment_counter(&mut self, key: &str) -> u32 {
502        let current = self
503            .get_memory(key)
504            .and_then(|v| v.parse::<u32>().ok())
505            .unwrap_or(0);
506        let next = current + 1;
507        self.set_memory(key, next.to_string());
508        next
509    }
510
511    /// Increments the "tasks_completed" counter in the agent's memory.
512    pub fn increment_tasks_completed(&mut self) -> u32 {
513        self.increment_counter("tasks_completed")
514    }
515
516    /// Executes a stateless conversation with the provided message history.
517    ///
518    /// This method creates a temporary chat session with the provided messages
519    /// and executes the agent logic without modifying the agent's persistent session.
520    /// This is useful for OpenAI API compatibility where each request contains
521    /// the full conversation history.
522    ///
523    /// # Arguments
524    ///
525    /// * `messages` - The complete conversation history for this request
526    /// * `temperature` - Optional temperature parameter for generation
527    /// * `max_tokens` - Optional maximum tokens parameter for generation
528    /// * `stop` - Optional stop sequences for generation
529    ///
530    /// # Returns
531    ///
532    /// A `Result` containing the assistant's response content.
533    pub async fn chat_with_history(
534        &mut self,
535        messages: Vec<ChatMessage>,
536        temperature: Option<f32>,
537        max_tokens: Option<u32>,
538        stop: Option<Vec<String>>,
539    ) -> Result<String> {
540        // Create a temporary session with the provided messages
541        let mut temp_session = ChatSession::new();
542
543        // Add all messages to the temporary session
544        for message in messages {
545            temp_session.add_message(message);
546        }
547
548        // Execute agent loop with tool calling using the temporary session
549        self.execute_with_tools_temp_session(temp_session, temperature, max_tokens, stop)
550            .await
551    }
552
553    /// Executes the agent's main loop with a temporary session, including tool calls.
554    async fn execute_with_tools_temp_session(
555        &mut self,
556        mut temp_session: ChatSession,
557        temperature: Option<f32>,
558        max_tokens: Option<u32>,
559        stop: Option<Vec<String>>,
560    ) -> Result<String> {
561        let mut iterations = 0;
562        let tool_definitions = self.tool_registry.get_definitions();
563
564        loop {
565            if iterations >= self.max_iterations {
566                return Err(HeliosError::AgentError(
567                    "Maximum iterations reached".to_string(),
568                ));
569            }
570
571            let messages = temp_session.get_messages();
572            let tools_option = if tool_definitions.is_empty() {
573                None
574            } else {
575                Some(tool_definitions.clone())
576            };
577
578            let response = self
579                .llm_client
580                .chat(
581                    messages,
582                    tools_option,
583                    temperature,
584                    max_tokens,
585                    stop.clone(),
586                )
587                .await?;
588
589            // Check if the response includes tool calls
590            if let Some(ref tool_calls) = response.tool_calls {
591                // Add assistant message with tool calls to temp session
592                temp_session.add_message(response.clone());
593
594                // Execute each tool call
595                for tool_call in tool_calls {
596                    let tool_name = &tool_call.function.name;
597                    let tool_args: Value = serde_json::from_str(&tool_call.function.arguments)
598                        .unwrap_or(Value::Object(serde_json::Map::new()));
599
600                    let tool_result = self
601                        .tool_registry
602                        .execute(tool_name, tool_args)
603                        .await
604                        .unwrap_or_else(|e| {
605                            ToolResult::error(format!("Tool execution failed: {}", e))
606                        });
607
608                    // Add tool result message to temp session
609                    let tool_message = ChatMessage::tool(tool_result.output, tool_call.id.clone());
610                    temp_session.add_message(tool_message);
611                }
612
613                iterations += 1;
614                continue;
615            }
616
617            // No tool calls, we have the final response
618            return Ok(response.content);
619        }
620    }
621
622    /// Executes a stateless conversation with the provided message history and streams the response.
623    ///
624    /// This method creates a temporary chat session with the provided messages
625    /// and streams the agent's response in real-time as tokens are generated.
626    /// Note: Tool calls are not supported in streaming mode yet - they will be
627    /// executed after the initial response is complete.
628    ///
629    /// # Arguments
630    ///
631    /// * `messages` - The complete conversation history for this request
632    /// * `temperature` - Optional temperature parameter for generation
633    /// * `max_tokens` - Optional maximum tokens parameter for generation
634    /// * `stop` - Optional stop sequences for generation
635    /// * `on_chunk` - Callback function called for each chunk of generated text
636    ///
637    /// # Returns
638    ///
639    /// A `Result` containing the final assistant message after streaming is complete.
640    pub async fn chat_stream_with_history<F>(
641        &mut self,
642        messages: Vec<ChatMessage>,
643        temperature: Option<f32>,
644        max_tokens: Option<u32>,
645        stop: Option<Vec<String>>,
646        on_chunk: F,
647    ) -> Result<ChatMessage>
648    where
649        F: FnMut(&str) + Send,
650    {
651        // Create a temporary session with the provided messages
652        let mut temp_session = ChatSession::new();
653
654        // Add all messages to the temporary session
655        for message in messages {
656            temp_session.add_message(message);
657        }
658
659        // For now, use streaming for the initial response only
660        // Tool calls will be handled after the stream completes
661        self.execute_streaming_with_tools_temp_session(
662            temp_session,
663            temperature,
664            max_tokens,
665            stop,
666            on_chunk,
667        )
668        .await
669    }
670
671    /// Executes the agent's main loop with streaming and a temporary session.
672    async fn execute_streaming_with_tools_temp_session<F>(
673        &mut self,
674        mut temp_session: ChatSession,
675        temperature: Option<f32>,
676        max_tokens: Option<u32>,
677        stop: Option<Vec<String>>,
678        mut on_chunk: F,
679    ) -> Result<ChatMessage>
680    where
681        F: FnMut(&str) + Send,
682    {
683        let mut iterations = 0;
684        let tool_definitions = self.tool_registry.get_definitions();
685
686        loop {
687            if iterations >= self.max_iterations {
688                return Err(HeliosError::AgentError(
689                    "Maximum iterations reached".to_string(),
690                ));
691            }
692
693            let messages = temp_session.get_messages();
694            let tools_option = if tool_definitions.is_empty() {
695                None
696            } else {
697                Some(tool_definitions.clone())
698            };
699
700            // Use streaming for all iterations
701            let mut streamed_content = String::new();
702
703            let stream_result = self
704                .llm_client
705                .chat_stream(
706                    messages,
707                    tools_option,
708                    temperature,
709                    max_tokens,
710                    stop.clone(),
711                    |chunk| {
712                        on_chunk(chunk);
713                        streamed_content.push_str(chunk);
714                    },
715                )
716                .await;
717
718            match stream_result {
719                Ok(response) => {
720                    // Check if the response includes tool calls
721                    if let Some(ref tool_calls) = response.tool_calls {
722                        // Add assistant message with tool calls to temp session
723                        let mut msg_with_content = response.clone();
724                        msg_with_content.content = streamed_content.clone();
725                        temp_session.add_message(msg_with_content);
726
727                        // Execute each tool call
728                        for tool_call in tool_calls {
729                            let tool_name = &tool_call.function.name;
730                            let tool_args: Value =
731                                serde_json::from_str(&tool_call.function.arguments)
732                                    .unwrap_or(Value::Object(serde_json::Map::new()));
733
734                            let tool_result = self
735                                .tool_registry
736                                .execute(tool_name, tool_args)
737                                .await
738                                .unwrap_or_else(|e| {
739                                    ToolResult::error(format!("Tool execution failed: {}", e))
740                                });
741
742                            // Add tool result message to temp session
743                            let tool_message =
744                                ChatMessage::tool(tool_result.output, tool_call.id.clone());
745                            temp_session.add_message(tool_message);
746                        }
747
748                        iterations += 1;
749                        continue; // Continue the loop for another iteration
750                    } else {
751                        // No tool calls, return the final response with streamed content
752                        let mut final_msg = response;
753                        final_msg.content = streamed_content;
754                        return Ok(final_msg);
755                    }
756                }
757                Err(e) => return Err(e),
758            }
759        }
760    }
761}
762
763pub struct AgentBuilder {
764    name: String,
765    config: Option<Config>,
766    system_prompt: Option<String>,
767    tools: Vec<Box<dyn crate::tools::Tool>>,
768    max_iterations: usize,
769    react_mode: bool,
770    react_prompt: Option<String>,
771}
772
773impl AgentBuilder {
774    pub fn new(name: impl Into<String>) -> Self {
775        Self {
776            name: name.into(),
777            config: None,
778            system_prompt: None,
779            tools: Vec::new(),
780            max_iterations: 10,
781            react_mode: false,
782            react_prompt: None,
783        }
784    }
785
786    pub fn config(mut self, config: Config) -> Self {
787        self.config = Some(config);
788        self
789    }
790
791    /// Shorthand: set config directly from a file or use defaults
792    pub fn auto_config(mut self) -> Self {
793        self.config = Some(Config::load_or_default("config.toml"));
794        self
795    }
796
797    pub fn system_prompt(mut self, prompt: impl Into<String>) -> Self {
798        self.system_prompt = Some(prompt.into());
799        self
800    }
801
802    /// Shorthand: use 'prompt' instead of 'system_prompt'
803    pub fn prompt(self, prompt: impl Into<String>) -> Self {
804        self.system_prompt(prompt)
805    }
806
807    /// Adds a single tool to the agent.
808    pub fn tool(mut self, tool: Box<dyn crate::tools::Tool>) -> Self {
809        self.tools.push(tool);
810        self
811    }
812
813    /// Shorthand: add a single tool (alias for `tool()`)
814    pub fn with_tool(mut self, tool: Box<dyn crate::tools::Tool>) -> Self {
815        self.tools.push(tool);
816        self
817    }
818
819    /// Adds multiple tools to the agent at once.
820    ///
821    /// # Example
822    ///
823    /// ```rust,no_run
824    /// # use helios_engine::{Agent, Config, CalculatorTool, EchoTool};
825    /// # async fn example() -> helios_engine::Result<()> {
826    /// # let config = Config::new_default();
827    /// let agent = Agent::builder("MyAgent")
828    ///     .config(config)
829    ///     .tools(vec![
830    ///         Box::new(CalculatorTool),
831    ///         Box::new(EchoTool),
832    ///     ])
833    ///     .build()
834    ///     .await?;
835    /// # Ok(())
836    /// # }
837    /// ```
838    pub fn tools(mut self, tools: Vec<Box<dyn crate::tools::Tool>>) -> Self {
839        self.tools.extend(tools);
840        self
841    }
842
843    /// Shorthand: add multiple tools (alias for `tools()`)
844    pub fn with_tools(mut self, tools: Vec<Box<dyn crate::tools::Tool>>) -> Self {
845        self.tools.extend(tools);
846        self
847    }
848
849    pub fn max_iterations(mut self, max: usize) -> Self {
850        self.max_iterations = max;
851        self
852    }
853
854    /// Enables ReAct mode for the agent.
855    ///
856    /// In ReAct mode, the agent will reason about the task and create a plan
857    /// before taking actions. This helps the agent think through problems
858    /// more systematically and make better decisions.
859    ///
860    /// # Example
861    ///
862    /// ```rust,no_run
863    /// # use helios_engine::{Agent, Config};
864    /// # async fn example() -> helios_engine::Result<()> {
865    /// # let config = Config::new_default();
866    /// let agent = Agent::builder("MyAgent")
867    ///     .config(config)
868    ///     .react()
869    ///     .build()
870    ///     .await?;
871    /// # Ok(())
872    /// # }
873    /// ```
874    pub fn react(mut self) -> Self {
875        self.react_mode = true;
876        self
877    }
878
879    /// Enables ReAct mode with a custom reasoning prompt.
880    ///
881    /// This allows you to customize how the agent reasons about tasks.
882    /// You can tailor the reasoning process to specific domains or tasks.
883    ///
884    /// # Arguments
885    ///
886    /// * `prompt` - Custom prompt to guide the agent's reasoning
887    ///
888    /// # Example
889    ///
890    /// ```rust,no_run
891    /// # use helios_engine::{Agent, Config};
892    /// # async fn example() -> helios_engine::Result<()> {
893    /// # let config = Config::new_default();
894    /// let custom_prompt = r#"
895    /// As a mathematical problem solver:
896    /// 1. Identify the mathematical operations needed
897    /// 2. Break down complex calculations into steps
898    /// 3. Determine the order of operations
899    /// 4. Plan which calculator functions to use
900    /// "#;
901    ///
902    /// let agent = Agent::builder("MathAgent")
903    ///     .config(config)
904    ///     .react_with_prompt(custom_prompt)
905    ///     .build()
906    ///     .await?;
907    /// # Ok(())
908    /// # }
909    /// ```
910    pub fn react_with_prompt(mut self, prompt: impl Into<String>) -> Self {
911        self.react_mode = true;
912        self.react_prompt = Some(prompt.into());
913        self
914    }
915
916    pub async fn build(self) -> Result<Agent> {
917        let config = self
918            .config
919            .ok_or_else(|| HeliosError::AgentError("Config is required".to_string()))?;
920
921        let mut agent = Agent::new(self.name, config).await?;
922
923        if let Some(prompt) = self.system_prompt {
924            agent.set_system_prompt(prompt);
925        }
926
927        for tool in self.tools {
928            agent.register_tool(tool);
929        }
930
931        agent.set_max_iterations(self.max_iterations);
932        agent.react_mode = self.react_mode;
933        agent.react_prompt = self.react_prompt;
934
935        Ok(agent)
936    }
937}
938
939#[cfg(test)]
940mod tests {
941    use super::*;
942    use crate::config::Config;
943    use crate::tools::{CalculatorTool, Tool, ToolParameter, ToolResult};
944    use serde_json::Value;
945    use std::collections::HashMap;
946
947    /// Tests that an agent can be created using the builder.
948    #[tokio::test]
949    async fn test_agent_creation_via_builder() {
950        let config = Config::new_default();
951        let agent = Agent::builder("test_agent").config(config).build().await;
952        assert!(agent.is_ok());
953    }
954
955    /// Tests the namespacing of agent memory.
956    #[tokio::test]
957    async fn test_agent_memory_namespacing_set_get_remove() {
958        let config = Config::new_default();
959        let mut agent = Agent::builder("test_agent")
960            .config(config)
961            .build()
962            .await
963            .unwrap();
964
965        // Set and get namespaced memory
966        agent.set_memory("working_directory", "/tmp");
967        assert_eq!(
968            agent.get_memory("working_directory"),
969            Some(&"/tmp".to_string())
970        );
971
972        // Ensure underlying chat session stored the prefixed key
973        assert_eq!(
974            agent.chat_session().get_metadata("agent:working_directory"),
975            Some(&"/tmp".to_string())
976        );
977        // Non-prefixed key should not exist
978        assert!(agent
979            .chat_session()
980            .get_metadata("working_directory")
981            .is_none());
982
983        // Remove should also be namespaced
984        let removed = agent.remove_memory("working_directory");
985        assert_eq!(removed.as_deref(), Some("/tmp"));
986        assert!(agent.get_memory("working_directory").is_none());
987    }
988
989    /// Tests that clearing agent memory only affects agent-scoped data.
990    #[tokio::test]
991    async fn test_agent_clear_memory_scoped() {
992        let config = Config::new_default();
993        let mut agent = Agent::builder("test_agent")
994            .config(config)
995            .build()
996            .await
997            .unwrap();
998
999        // Set an agent memory and a general (non-agent) session metadata key
1000        agent.set_memory("tasks_completed", "3");
1001        agent
1002            .chat_session_mut()
1003            .set_metadata("session_start", "now");
1004
1005        // Clear only agent-scoped memory
1006        agent.clear_memory();
1007
1008        // Agent memory removed
1009        assert!(agent.get_memory("tasks_completed").is_none());
1010        // General session metadata preserved
1011        assert_eq!(
1012            agent.chat_session().get_metadata("session_start"),
1013            Some(&"now".to_string())
1014        );
1015    }
1016
1017    /// Tests the increment helper methods for agent memory.
1018    #[tokio::test]
1019    async fn test_agent_increment_helpers() {
1020        let config = Config::new_default();
1021        let mut agent = Agent::builder("test_agent")
1022            .config(config)
1023            .build()
1024            .await
1025            .unwrap();
1026
1027        // tasks_completed increments from 0
1028        let n1 = agent.increment_tasks_completed();
1029        assert_eq!(n1, 1);
1030        assert_eq!(agent.get_memory("tasks_completed"), Some(&"1".to_string()));
1031
1032        let n2 = agent.increment_tasks_completed();
1033        assert_eq!(n2, 2);
1034        assert_eq!(agent.get_memory("tasks_completed"), Some(&"2".to_string()));
1035
1036        // generic counter
1037        let f1 = agent.increment_counter("files_accessed");
1038        assert_eq!(f1, 1);
1039        let f2 = agent.increment_counter("files_accessed");
1040        assert_eq!(f2, 2);
1041        assert_eq!(agent.get_memory("files_accessed"), Some(&"2".to_string()));
1042    }
1043
1044    /// Tests the full functionality of the agent builder.
1045    #[tokio::test]
1046    async fn test_agent_builder() {
1047        let config = Config::new_default();
1048        let agent = Agent::builder("test_agent")
1049            .config(config)
1050            .system_prompt("You are a helpful assistant")
1051            .max_iterations(5)
1052            .tool(Box::new(CalculatorTool))
1053            .build()
1054            .await
1055            .unwrap();
1056
1057        assert_eq!(agent.name(), "test_agent");
1058        assert_eq!(agent.max_iterations, 5);
1059        assert_eq!(
1060            agent.tool_registry().list_tools(),
1061            vec!["calculator".to_string()]
1062        );
1063    }
1064
1065    /// Tests setting the system prompt for an agent.
1066    #[tokio::test]
1067    async fn test_agent_system_prompt() {
1068        let config = Config::new_default();
1069        let mut agent = Agent::builder("test_agent")
1070            .config(config)
1071            .build()
1072            .await
1073            .unwrap();
1074        agent.set_system_prompt("You are a test agent");
1075
1076        // Check that the system prompt is set in chat session
1077        let session = agent.chat_session();
1078        assert_eq!(
1079            session.system_prompt,
1080            Some("You are a test agent".to_string())
1081        );
1082    }
1083
1084    /// Tests the tool registry functionality of an agent.
1085    #[tokio::test]
1086    async fn test_agent_tool_registry() {
1087        let config = Config::new_default();
1088        let mut agent = Agent::builder("test_agent")
1089            .config(config)
1090            .build()
1091            .await
1092            .unwrap();
1093
1094        // Initially no tools
1095        assert!(agent.tool_registry().list_tools().is_empty());
1096
1097        // Register a tool
1098        agent.register_tool(Box::new(CalculatorTool));
1099        assert_eq!(
1100            agent.tool_registry().list_tools(),
1101            vec!["calculator".to_string()]
1102        );
1103    }
1104
1105    /// Tests clearing the chat history of an agent.
1106    #[tokio::test]
1107    async fn test_agent_clear_history() {
1108        let config = Config::new_default();
1109        let mut agent = Agent::builder("test_agent")
1110            .config(config)
1111            .build()
1112            .await
1113            .unwrap();
1114
1115        // Add a message to the chat session
1116        agent.chat_session_mut().add_user_message("Hello");
1117        assert!(!agent.chat_session().messages.is_empty());
1118
1119        // Clear history
1120        agent.clear_history();
1121        assert!(agent.chat_session().messages.is_empty());
1122    }
1123
1124    // Mock tool for testing
1125    struct MockTool;
1126
1127    #[async_trait::async_trait]
1128    impl Tool for MockTool {
1129        fn name(&self) -> &str {
1130            "mock_tool"
1131        }
1132
1133        fn description(&self) -> &str {
1134            "A mock tool for testing"
1135        }
1136
1137        fn parameters(&self) -> HashMap<String, ToolParameter> {
1138            let mut params = HashMap::new();
1139            params.insert(
1140                "input".to_string(),
1141                ToolParameter {
1142                    param_type: "string".to_string(),
1143                    description: "Input parameter".to_string(),
1144                    required: Some(true),
1145                },
1146            );
1147            params
1148        }
1149
1150        async fn execute(&self, args: Value) -> crate::Result<ToolResult> {
1151            let input = args
1152                .get("input")
1153                .and_then(|v| v.as_str())
1154                .unwrap_or("default");
1155            Ok(ToolResult::success(format!("Mock tool output: {}", input)))
1156        }
1157    }
1158}