neuromance_agent/
lib.rs

1//! # neuromance-agent
2//!
3//! Agent framework for autonomous task execution with LLMs.
4//!
5//! This crate provides high-level abstractions for building autonomous agents that can
6//! execute multi-step tasks, maintain state and memory, and use tools to accomplish goals.
7//! Agents wrap the lower-level [`neuromance::Core`] functionality with task management,
8//! state persistence, and sequential execution capabilities.
9//!
10//! ## Core Components
11//!
12//! - [`Agent`]: Trait defining the agent interface with state management and execution
13//! - [`BaseAgent`]: Default implementation with conversation history and tool support
14//! - [`AgentBuilder`]: Fluent builder for constructing agents with custom configuration
15//! - [`AgentTask`]: Task abstraction for defining agent objectives and validation
16//!
17//! ## Agent State Management
18//!
19//! Agents maintain several types of state (from [`neuromance_common::agents`]):
20//!
21//! - **Conversation History**: Full message history and responses
22//! - **Memory**: Short-term and long-term memory with working memory for active data
23//! - **Context**: Task definition, goals, constraints, and environment variables
24//! - **Statistics**: Execution metrics like token usage and tool call counts
25//!
26//! ## Example: Creating and Running an Agent
27//!
28//! ```rust,ignore
29//! use neuromance_agent::{BaseAgent, Agent};
30//! use neuromance::Core;
31//! use neuromance_client::OpenAIClient;
32//! use neuromance_common::{Config, Message};
33//!
34//! # async fn example() -> anyhow::Result<()> {
35//! // Create an LLM client
36//! let config = Config::new("openai", "gpt-4")
37//!     .with_api_key("sk-...");
38//! let client = OpenAIClient::new(config)?;
39//!
40//! // Build an agent
41//! let mut agent = BaseAgent::builder("research-agent", client)
42//!     .with_system_prompt("You are a research assistant that finds information.")
43//!     .with_user_prompt("Find the population of Tokyo.")
44//!     .build()?;
45//!
46//! // Execute the agent
47//! let response = agent.execute(None).await?;
48//! println!("Agent response: {}", response.content.content);
49//! # Ok(())
50//! # }
51//! ```
52//!
53//! ## Example: Using the Agent Builder
54//!
55//! The [`AgentBuilder`] provides a fluent API for agent configuration:
56//!
57//! ```rust,ignore
58//! use neuromance_agent::BaseAgent;
59//! use neuromance_client::OpenAIClient;
60//! use neuromance_common::Config;
61//!
62//! # async fn example() -> anyhow::Result<()> {
63//! let config = Config::new("openai", "gpt-4o-mini");
64//! let client = OpenAIClient::new(config)?;
65//!
66//! let agent = BaseAgent::builder("task-agent", client)
67//!     .with_system_prompt("You are a task completion agent.")
68//!     .with_user_prompt("Complete the following task: organize these files.")
69//!     .with_max_turns(5)
70//!     .with_auto_approve_tools(true)
71//!     .build()?;
72//! # Ok(())
73//! # }
74//! ```
75//!
76//! ## Task-Based Execution
77//!
78//! The [`task`] module provides task abstractions for defining agent objectives:
79//!
80//! ```rust,ignore
81//! use neuromance_agent::{AgentTask, BaseAgent};
82//! use neuromance_common::Message;
83//!
84//! # async fn example() -> anyhow::Result<()> {
85//! # let mut agent = unimplemented!();
86//! // Define a task with validation
87//! let task = AgentTask::new("research_task")
88//!     .with_description("Research the history of Rust programming language")
89//!     .with_validation(|response| {
90//!         // Custom validation logic
91//!         Ok(response.content.content.len() > 100)
92//!     });
93//!
94//! // Execute the task
95//! let response = task.execute(&mut agent).await?;
96//! # Ok(())
97//! # }
98//! ```
99//!
100//! ## Agent Lifecycle
101//!
102//! Agents follow a standard lifecycle:
103//!
104//! 1. **Creation**: Built with configuration and system/user prompts
105//! 2. **Execution**: Process messages through LLM with tool support
106//! 3. **State Updates**: Maintain conversation history and statistics
107//! 4. **Reset**: Clear state for fresh execution (via [`Agent::reset`])
108//!
109//! ## Tool Integration
110//!
111//! Agents automatically integrate with [`neuromance_tools`] for tool execution.
112//! Tools can be added to the agent's [`Core`] instance and will be available
113//! during execution:
114//!
115//! ```rust,ignore
116//! use neuromance_agent::BaseAgent;
117//! use neuromance_tools::{ToolExecutor, ThinkTool};
118//!
119//! # async fn example() -> anyhow::Result<()> {
120//! # let client = unimplemented!();
121//! let mut agent = BaseAgent::new("agent-id".to_string(), Core::new(client));
122//!
123//! // Add tools to the agent's core
124//! agent.core.tool_executor.add_tool(ThinkTool);
125//!
126//! // Tools are now available during execution
127//! # Ok(())
128//! # }
129//! ```
130//!
131//! ## Memory and Context
132//!
133//! Agents maintain structured state via [`AgentState`]:
134//!
135//! - **Memory**: Stores short-term context and long-term knowledge
136//! - **Context**: Task definition, goals, constraints, environment
137//! - **Stats**: Execution metrics for monitoring and debugging
138//!
139//! This state can be serialized for persistence or debugging.
140
141use anyhow::Result;
142use async_trait::async_trait;
143use log::info;
144use uuid::Uuid;
145
146use neuromance::Core;
147use neuromance_client::LLMClient;
148use neuromance_common::agents::{AgentContext, AgentMemory, AgentResponse, AgentState, AgentStats};
149use neuromance_common::chat::{Message, MessageRole};
150use neuromance_common::client::ToolChoice;
151
152pub mod builder;
153pub mod task;
154
155pub use builder::AgentBuilder;
156pub use task::{AgentTask, TaskResponse, TaskState};
157
158/// Base agent implementation with common functionality
159pub struct BaseAgent<C: LLMClient> {
160    pub id: String,
161    pub conversation_id: Uuid,
162    pub core: Core<C>,
163    pub state: AgentState,
164    pub system_prompt: Option<String>,
165    pub user_prompt: Option<String>,
166    pub messages: Vec<Message>,
167    pub tool_choice: ToolChoice,
168}
169
170impl<C: LLMClient> BaseAgent<C> {
171    pub fn new(id: String, core: Core<C>) -> Self {
172        Self {
173            id,
174            conversation_id: Uuid::new_v4(),
175            core,
176            state: AgentState::default(),
177            system_prompt: None,
178            user_prompt: None,
179            messages: Vec::<Message>::new(),
180            tool_choice: ToolChoice::Auto,
181        }
182    }
183
184    pub fn builder(id: impl Into<String>, client: C) -> AgentBuilder<C> {
185        AgentBuilder::new(id, client)
186    }
187}
188
189#[async_trait]
190impl<C: LLMClient + Send + Sync> Agent for BaseAgent<C> {
191    fn id(&self) -> &str {
192        &self.id
193    }
194
195    fn state(&self) -> &AgentState {
196        &self.state
197    }
198
199    fn state_mut(&mut self) -> &mut AgentState {
200        &mut self.state
201    }
202
203    async fn reset(&mut self) -> Result<()> {
204        self.state.conversation_history.clear();
205        self.state.memory = AgentMemory::default();
206        self.state.context = AgentContext::default();
207        self.state.stats = AgentStats::default();
208        self.conversation_id = Uuid::new_v4();
209        self.messages = Vec::<Message>::new();
210        Ok(())
211    }
212
213    async fn execute(&mut self, messages: Option<Vec<Message>>) -> Result<AgentResponse> {
214        info!("Agent {} executing", self.id);
215        self.core.auto_approve_tools = true;
216        self.core.max_turns = Some(3);
217        self.core.tool_choice = self.tool_choice.clone();
218
219        // Use provided messages or fall back to stored messages
220        let messages = messages.unwrap_or_else(|| self.messages.clone());
221
222        // Validate that we have at least system and user messages
223        if messages.len() < 2 {
224            return Err(anyhow::anyhow!(
225                "Agent requires at least a system message and user message to execute"
226            ));
227        }
228
229        if messages[0].role != MessageRole::System {
230            return Err(anyhow::anyhow!(
231                "First message must be a system message, found: {:?}",
232                messages[0].role
233            ));
234        }
235
236        if messages[1].role != MessageRole::User {
237            return Err(anyhow::anyhow!(
238                "Second message must be a user message, found: {:?}",
239                messages[1].role
240            ));
241        }
242
243        let messages = self.core.chat_with_tool_loop(messages).await?;
244
245        // Extract the final assistant message and tool responses
246        let content = messages
247            .iter()
248            .filter(|m| m.role == MessageRole::Assistant)
249            .next_back()
250            .cloned()
251            .unwrap_or_else(|| {
252                Message::new(
253                    self.conversation_id,
254                    MessageRole::Assistant,
255                    "No final response generated".to_string(),
256                )
257            });
258
259        let tool_responses = messages
260            .iter()
261            .filter(|m| m.role == MessageRole::Tool)
262            .cloned()
263            .collect();
264
265        Ok(AgentResponse {
266            content,
267            reasoning: None,
268            tool_responses,
269        })
270    }
271}
272
273#[async_trait]
274pub trait Agent: Send + Sync {
275    /// Returns the unique identifier of the agent.
276    fn id(&self) -> &str;
277
278    /// Returns immutable reference to the agent's current state.
279    fn state(&self) -> &AgentState;
280
281    /// Returns mutable reference to the agent's current state.
282    fn state_mut(&mut self) -> &mut AgentState;
283
284    /// Resets the agent to its initial state.
285    ///
286    /// # Returns
287    /// A result indicating success or failure of the reset operation
288    async fn reset(&mut self) -> Result<()>;
289
290    /// Execute core chat with tools loop
291    async fn execute(&mut self, messages: Option<Vec<Message>>) -> Result<AgentResponse>;
292}