Skip to main content

mixtape_core/agent/
mod.rs

1//! Agent module for orchestrating LLM interactions with tools
2//!
3//! The Agent is the core orchestrator that manages conversations with language models,
4//! executes tools, handles permission workflows, and maintains session state.
5
6mod builder;
7mod context;
8mod helpers;
9#[cfg(feature = "mcp")]
10mod mcp;
11mod permission;
12mod run;
13mod streaming;
14mod tools;
15mod types;
16
17#[cfg(feature = "session")]
18mod session;
19
20// Re-export public types
21pub use builder::AgentBuilder;
22pub use context::{ContextConfig, ContextError, ContextLoadResult, ContextSource};
23pub use types::{
24    AgentError, AgentResponse, PermissionError, TokenUsageStats, ToolCallInfo, ToolInfo,
25    DEFAULT_MAX_CONCURRENT_TOOLS, DEFAULT_PERMISSION_TIMEOUT,
26};
27
28#[cfg(feature = "session")]
29pub use types::SessionInfo;
30
31use std::collections::HashMap;
32use std::sync::atomic::{AtomicU64, Ordering};
33use std::sync::Arc;
34use std::time::Duration;
35use tokio::sync::{mpsc, RwLock};
36
37use crate::conversation::BoxedConversationManager;
38use crate::events::{AgentEvent, AgentHook, HookId};
39use crate::permission::{AuthorizationResponse, ToolCallAuthorizer};
40use crate::provider::ModelProvider;
41use crate::tool::DynTool;
42use crate::types::Message;
43
44#[cfg(feature = "session")]
45use crate::session::SessionStore;
46
47/// Agent that orchestrates interactions between a language model and tools
48///
49/// Create an agent using the builder pattern:
50///
51/// ```ignore
52/// use mixtape_core::{Agent, ClaudeSonnet4_5, Result};
53///
54/// #[tokio::main]
55/// async fn main() -> Result<()> {
56///     let agent = Agent::builder()
57///         .bedrock(ClaudeSonnet4_5)
58///         .with_system_prompt("You are a helpful assistant")
59///         .build()
60///         .await?;
61///
62///     let response = agent.run("Hello!").await?;
63///     println!("{}", response);
64///     Ok(())
65/// }
66/// ```
67pub struct Agent {
68    pub(super) provider: Arc<dyn ModelProvider>,
69    pub(super) system_prompt: Option<String>,
70    pub(super) max_concurrent_tools: usize,
71    pub(super) tools: Vec<Box<dyn DynTool>>,
72    pub(super) hooks: Arc<parking_lot::RwLock<HashMap<HookId, Arc<dyn AgentHook>>>>,
73    pub(super) next_hook_id: AtomicU64,
74    /// Tool call authorizer (always present, uses MemoryGrantStore by default)
75    pub(super) authorizer: Arc<RwLock<ToolCallAuthorizer>>,
76    /// Timeout for authorization requests
77    pub(super) authorization_timeout: Duration,
78    /// Pending authorization requests
79    pub(super) pending_authorizations:
80        Arc<RwLock<HashMap<String, mpsc::Sender<AuthorizationResponse>>>>,
81    /// MCP clients for graceful shutdown
82    #[cfg(feature = "mcp")]
83    pub(super) mcp_clients: Vec<Arc<crate::mcp::McpClient>>,
84    /// Conversation manager for context window handling
85    pub(super) conversation_manager: parking_lot::RwLock<BoxedConversationManager>,
86
87    #[cfg(feature = "session")]
88    pub(super) session_store: Option<Arc<dyn SessionStore>>,
89
90    // Context file fields
91    /// Context file sources (resolved at runtime)
92    pub(super) context_sources: Vec<ContextSource>,
93    /// Context configuration (size limits)
94    pub(super) context_config: ContextConfig,
95    /// Last context load result (for inspection)
96    pub(super) last_context_result: parking_lot::RwLock<Option<ContextLoadResult>>,
97}
98
99impl Agent {
100    /// Add an event hook to observe agent execution.
101    ///
102    /// Returns a [`HookId`] that can be used to remove the hook later via [`remove_hook`](Self::remove_hook).
103    ///
104    /// Hooks receive notifications about agent lifecycle, model calls,
105    /// and tool executions in real-time.
106    ///
107    /// # Example
108    /// ```ignore
109    /// use mixtape_core::{Agent, ClaudeSonnet4_5, AgentEvent, AgentHook};
110    ///
111    /// struct Logger;
112    ///
113    /// impl AgentHook for Logger {
114    ///     fn on_event(&self, event: &AgentEvent) {
115    ///         println!("Event: {:?}", event);
116    ///     }
117    /// }
118    ///
119    /// let agent = Agent::builder()
120    ///     .bedrock(ClaudeSonnet4_5)
121    ///     .build()
122    ///     .await?;
123    /// let hook_id = agent.add_hook(Logger);
124    ///
125    /// // Later, remove the hook
126    /// agent.remove_hook(hook_id);
127    /// ```
128    pub fn add_hook(&self, hook: impl AgentHook + 'static) -> HookId {
129        let id = HookId(self.next_hook_id.fetch_add(1, Ordering::SeqCst));
130        self.hooks.write().insert(id, Arc::new(hook));
131        id
132    }
133
134    /// Remove a previously registered hook.
135    ///
136    /// Returns `true` if the hook was found and removed, `false` otherwise.
137    pub fn remove_hook(&self, id: HookId) -> bool {
138        self.hooks.write().remove(&id).is_some()
139    }
140
141    /// Emit an event to all registered hooks
142    pub(crate) fn emit_event(&self, event: AgentEvent) {
143        let hooks = self.hooks.read();
144        for hook in hooks.values() {
145            hook.on_event(&event);
146        }
147    }
148
149    /// Get the model name for display
150    pub fn model_name(&self) -> &str {
151        self.provider.name()
152    }
153
154    /// Gracefully shutdown the agent, disconnecting MCP servers
155    ///
156    /// Call this before dropping the agent to ensure clean subprocess termination.
157    pub async fn shutdown(&self) {
158        #[cfg(feature = "mcp")]
159        for client in &self.mcp_clients {
160            let _ = client.disconnect().await;
161        }
162    }
163
164    /// Get current context usage information
165    ///
166    /// Returns statistics about how much of the context window is being used,
167    /// including the number of messages and estimated token count.
168    pub fn get_context_usage(&self) -> crate::conversation::ContextUsage {
169        let limits = crate::conversation::ContextLimits::new(self.provider.max_context_tokens());
170        let provider = &self.provider;
171        let estimate_tokens = |msgs: &[Message]| provider.estimate_message_tokens(msgs);
172
173        self.conversation_manager
174            .read()
175            .context_usage(limits, &estimate_tokens)
176    }
177
178    /// Get information about the most recently loaded context files
179    ///
180    /// Returns `None` if `run()` has not been called yet.
181    ///
182    /// # Example
183    /// ```ignore
184    /// let response = agent.run("Hello").await?;
185    ///
186    /// if let Some(ctx) = agent.last_context_info() {
187    ///     println!("Loaded {} context files ({} bytes)",
188    ///         ctx.files.len(), ctx.total_bytes);
189    ///     for file in &ctx.files {
190    ///         println!("  - {}", file.resolved_path.display());
191    ///     }
192    /// }
193    /// ```
194    pub fn last_context_info(&self) -> Option<ContextLoadResult> {
195        self.last_context_result.read().clone()
196    }
197}