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::Arc;
33use std::time::Duration;
34use tokio::sync::{mpsc, RwLock};
35
36use crate::conversation::BoxedConversationManager;
37use crate::events::{AgentEvent, AgentHook};
38use crate::permission::{AuthorizationResponse, ToolCallAuthorizer};
39use crate::provider::ModelProvider;
40use crate::tool::DynTool;
41use crate::types::Message;
42
43#[cfg(feature = "session")]
44use crate::session::SessionStore;
45
46/// Agent that orchestrates interactions between a language model and tools
47///
48/// Create an agent using the builder pattern:
49///
50/// ```ignore
51/// use mixtape_core::{Agent, ClaudeSonnet4_5, Result};
52///
53/// #[tokio::main]
54/// async fn main() -> Result<()> {
55///     let agent = Agent::builder()
56///         .bedrock(ClaudeSonnet4_5)
57///         .with_system_prompt("You are a helpful assistant")
58///         .build()
59///         .await?;
60///
61///     let response = agent.run("Hello!").await?;
62///     println!("{}", response);
63///     Ok(())
64/// }
65/// ```
66pub struct Agent {
67    pub(super) provider: Arc<dyn ModelProvider>,
68    pub(super) system_prompt: Option<String>,
69    pub(super) max_concurrent_tools: usize,
70    pub(super) tools: Vec<Box<dyn DynTool>>,
71    pub(super) hooks: Arc<parking_lot::RwLock<Vec<Arc<dyn AgentHook>>>>,
72    /// Tool call authorizer (always present, uses MemoryGrantStore by default)
73    pub(super) authorizer: Arc<RwLock<ToolCallAuthorizer>>,
74    /// Timeout for authorization requests
75    pub(super) authorization_timeout: Duration,
76    /// Pending authorization requests
77    pub(super) pending_authorizations:
78        Arc<RwLock<HashMap<String, mpsc::Sender<AuthorizationResponse>>>>,
79    /// MCP clients for graceful shutdown
80    #[cfg(feature = "mcp")]
81    pub(super) mcp_clients: Vec<Arc<crate::mcp::McpClient>>,
82    /// Conversation manager for context window handling
83    pub(super) conversation_manager: parking_lot::RwLock<BoxedConversationManager>,
84
85    #[cfg(feature = "session")]
86    pub(super) session_store: Option<Arc<dyn SessionStore>>,
87
88    // Context file fields
89    /// Context file sources (resolved at runtime)
90    pub(super) context_sources: Vec<ContextSource>,
91    /// Context configuration (size limits)
92    pub(super) context_config: ContextConfig,
93    /// Last context load result (for inspection)
94    pub(super) last_context_result: parking_lot::RwLock<Option<ContextLoadResult>>,
95}
96
97impl Agent {
98    /// Add an event hook to observe agent execution
99    ///
100    /// Hooks receive notifications about agent lifecycle, model calls,
101    /// and tool executions in real-time.
102    ///
103    /// # Example
104    /// ```ignore
105    /// use mixtape_core::{Agent, ClaudeSonnet4_5, AgentEvent, AgentHook};
106    ///
107    /// struct Logger;
108    ///
109    /// impl AgentHook for Logger {
110    ///     fn on_event(&self, event: &AgentEvent) {
111    ///         println!("Event: {:?}", event);
112    ///     }
113    /// }
114    ///
115    /// let agent = Agent::builder()
116    ///     .bedrock(ClaudeSonnet4_5)
117    ///     .build()
118    ///     .await?;
119    /// agent.add_hook(Logger);
120    /// ```
121    pub fn add_hook(&self, hook: impl AgentHook + 'static) {
122        self.hooks.write().push(Arc::new(hook));
123    }
124
125    /// Emit an event to all registered hooks
126    pub(crate) fn emit_event(&self, event: AgentEvent) {
127        let hooks = self.hooks.read();
128        for hook in hooks.iter() {
129            hook.on_event(&event);
130        }
131    }
132
133    /// Get the model name for display
134    pub fn model_name(&self) -> &str {
135        self.provider.name()
136    }
137
138    /// Gracefully shutdown the agent, disconnecting MCP servers
139    ///
140    /// Call this before dropping the agent to ensure clean subprocess termination.
141    pub async fn shutdown(&self) {
142        #[cfg(feature = "mcp")]
143        for client in &self.mcp_clients {
144            let _ = client.disconnect().await;
145        }
146    }
147
148    /// Get current context usage information
149    ///
150    /// Returns statistics about how much of the context window is being used,
151    /// including the number of messages and estimated token count.
152    pub fn get_context_usage(&self) -> crate::conversation::ContextUsage {
153        let limits = crate::conversation::ContextLimits::new(self.provider.max_context_tokens());
154        let provider = &self.provider;
155        let estimate_tokens = |msgs: &[Message]| provider.estimate_message_tokens(msgs);
156
157        self.conversation_manager
158            .read()
159            .context_usage(limits, &estimate_tokens)
160    }
161
162    /// Get information about the most recently loaded context files
163    ///
164    /// Returns `None` if `run()` has not been called yet.
165    ///
166    /// # Example
167    /// ```ignore
168    /// let response = agent.run("Hello").await?;
169    ///
170    /// if let Some(ctx) = agent.last_context_info() {
171    ///     println!("Loaded {} context files ({} bytes)",
172    ///         ctx.files.len(), ctx.total_bytes);
173    ///     for file in &ctx.files {
174    ///         println!("  - {}", file.resolved_path.display());
175    ///     }
176    /// }
177    /// ```
178    pub fn last_context_info(&self) -> Option<ContextLoadResult> {
179        self.last_context_result.read().clone()
180    }
181}