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}