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}