use crate::api::ApiClient; use crate::costtracker::CostTracker; use crate::hooks::HookConfig; use crate::mcp::{self, McpClient}; use crate::tools::ToolRegistry; use crate::types::*; use serde_json::Value; use std::collections::HashMap; use std::sync::Arc; use tokio::sync::mpsc; use tokio_util::sync::CancellationToken; /// Configuration options for creating an agent. pub struct AgentOptions { /// LLM model ID (default: claude-sonnet-4-6-20250514). pub model: Option, /// API key for authentication. pub api_key: Option, /// Custom API base URL. pub base_url: Option, /// Custom LLM provider (bypasses built-in ApiClient creation). pub provider: Option>, /// Working directory for tool execution. pub cwd: Option, /// Custom system prompt (replaces default). pub system_prompt: Option, /// Additional text appended to the default system prompt. pub append_system_prompt: Option, /// Skills context summary for system prompt injection. pub skills_summary: Option, /// Maximum number of agentic turns (default: 100). pub max_turns: Option, /// Maximum budget in USD. pub max_budget_usd: Option, /// Permission mode for tool access control. pub permission_mode: Option, /// Tool names to allow (whitelist). pub allowed_tools: Option>, /// Tool names to deny (blacklist). pub disallowed_tools: Option>, /// Custom permission callback. pub can_use_tool: Option, /// Callback for user interaction (AskUserQuestion tool). pub ask_fn: Option, /// Custom tools to register. pub custom_tools: Vec>, /// MCP server configurations. pub mcp_servers: HashMap, /// Subagent definitions. pub agents: HashMap, /// Extended thinking configuration. pub thinking: Option, /// Maximum output tokens per response. pub max_tokens: Option, /// Structured output JSON schema. pub json_schema: Option, /// Hook configuration. pub hooks: Option, /// Cancellation token used to stop the running agent loop and tools. pub abort_signal: Option, /// Custom HTTP headers for API requests. pub custom_headers: HashMap, /// Resume the most recent session in the current working directory. pub continue_session: Option, /// Resume a specific session by ID. pub resume: Option, /// Fork instead of continuing a session. pub fork_session: Option, /// Control whether the session is persisted to disk. pub persist_session: Option, /// Explicit session ID to use (instead of auto-generating one). pub session_id: Option, /// Effort level for reasoning: "low", "medium", "high", "max". pub effort: Option, /// Fallback model if primary is unavailable. pub fallback_model: Option, /// Sandbox configuration for restricting tool execution. pub sandbox: Option, /// Enable debug mode. pub debug: Option, /// Additional working directories. pub additional_directories: Option>, } impl Default for AgentOptions { fn default() -> Self { Self { model: None, api_key: None, base_url: None, provider: None, cwd: None, system_prompt: None, append_system_prompt: None, skills_summary: None, max_turns: None, max_budget_usd: None, permission_mode: None, allowed_tools: None, disallowed_tools: None, can_use_tool: None, ask_fn: None, custom_tools: Vec::new(), mcp_servers: HashMap::new(), agents: HashMap::new(), thinking: None, max_tokens: None, json_schema: None, hooks: None, abort_signal: None, custom_headers: HashMap::new(), continue_session: None, resume: None, fork_session: None, persist_session: None, session_id: None, effort: None, fallback_model: None, sandbox: None, debug: None, additional_directories: None, } } } /// Subagent definition. #[derive(Debug, Clone)] pub struct SubagentDefinition { pub description: String, pub instructions: Option, pub tools: Option>, pub model: Option, } /// The main Agent struct that orchestrates the agentic loop. pub struct Agent { pub(crate) api_client: ApiClient, pub(crate) registry: ToolRegistry, /// Conversation messages. pub messages: Vec, /// Working directory. pub cwd: String, /// Custom system prompt. pub system_prompt: Option, /// Additional system prompt text. pub append_system_prompt: Option, /// Skills context summary. pub skills_summary: Option, /// Maximum agentic turns. pub max_turns: u32, /// Maximum budget in USD. pub max_budget_usd: Option, pub(crate) cost_tracker: CostTracker, pub(crate) mcp_client: Arc, /// Subagent definitions. pub agents: HashMap, /// Extended thinking configuration. pub thinking: Option, /// Maximum output tokens. pub max_tokens: Option, /// Structured output JSON schema. pub json_schema: Option, /// Hook configuration. pub hooks: HookConfig, pub(crate) can_use_tool: Option, pub(crate) session_id: String, pub(crate) abort_signal: CancellationToken, } impl Agent { /// Create a new agent with the given options. pub async fn new(options: AgentOptions) -> Result { let cwd = options.cwd.unwrap_or_else(|| { std::env::current_dir() .unwrap() .to_string_lossy() .to_string() }); let api_client = if let Some(provider) = options.provider { ApiClient::with_provider(provider, options.model) } else { ApiClient::new(options.api_key, options.base_url, options.model) }; let mut registry = ToolRegistry::default_registry(); // Replace default AskUserTool with configured one if ask_fn is provided if let Some(ask_fn) = options.ask_fn { registry.register_replace(Arc::new(crate::tools::askuser::AskUserTool::new(ask_fn))); } // Register custom tools for tool in options.custom_tools { registry.register(tool); } // Apply allowed/disallowed tool filters if let Some(allowed) = &options.allowed_tools { let allowed_refs: Vec<&str> = allowed.iter().map(|s| s.as_str()).collect(); registry.retain(&allowed_refs); } if let Some(disallowed) = &options.disallowed_tools { let disallowed_refs: Vec<&str> = disallowed.iter().map(|s| s.as_str()).collect(); registry.remove(&disallowed_refs); } // Connect MCP servers let mcp_client = Arc::new(McpClient::new()); for (name, config) in &options.mcp_servers { match mcp_client.connect(name, config.clone()).await { Ok(tools) => { let mcp_tools = mcp::create_mcp_tools(name, &tools, mcp_client.clone()); for tool in mcp_tools { registry.register(tool); } } Err(e) => { tracing::warn!("Failed to connect MCP server '{}': {}", name, e); } } } let session_id = uuid::Uuid::new_v4().to_string(); Ok(Self { api_client, registry, messages: Vec::new(), cwd, system_prompt: options.system_prompt, append_system_prompt: options.append_system_prompt, skills_summary: options.skills_summary, max_turns: options.max_turns.unwrap_or(100), max_budget_usd: options.max_budget_usd, cost_tracker: CostTracker::new(), mcp_client, agents: options.agents, thinking: options.thinking, max_tokens: options.max_tokens, json_schema: options.json_schema, hooks: options.hooks.unwrap_or_default(), can_use_tool: options.can_use_tool, session_id, abort_signal: options.abort_signal.unwrap_or_default(), }) } /// Execute a query and return a channel of streaming SDK messages. pub async fn query( &mut self, prompt: &str, ) -> (mpsc::Receiver, tokio::task::JoinHandle<()>) { let (tx, rx) = mpsc::channel(100); // Add user message let user_msg = crate::utils::messages::create_user_message(prompt); self.messages.push(user_msg); // Clone what we need for the async loop let messages = self.messages.clone(); let api_client = self.api_client.clone(); let cwd = self.cwd.clone(); let system_prompt = self.system_prompt.clone(); let append_system_prompt = self.append_system_prompt.clone(); let skills_summary = self.skills_summary.clone(); let max_turns = self.max_turns; let max_budget_usd = self.max_budget_usd; let cost_tracker = self.cost_tracker.clone(); let thinking = self.thinking.clone(); let max_tokens = self.max_tokens; let registry = std::sync::Arc::new(self.registry_snapshot()); let can_use_tool = self.can_use_tool.clone(); let abort_signal = self.abort_signal.clone(); let handle = tokio::spawn(async move { let result: Result, String> = super::r#loop::run_loop( api_client, messages, registry, &cwd, system_prompt.as_deref(), append_system_prompt.as_deref(), skills_summary.as_deref(), max_turns, max_budget_usd, &cost_tracker, thinking, max_tokens, can_use_tool.as_ref(), abort_signal, tx.clone(), ) .await; if let Err(e) = result { let _ = tx .send(SDKMessage::Error { message: e.to_string(), }) .await; } }); (rx, handle) } /// Blocking query that returns a complete QueryResult. pub async fn prompt(&mut self, text: &str) -> Result { let start = std::time::Instant::now(); let (mut rx, handle) = self.query(text).await; let mut result_text = String::new(); let mut usage = Usage::default(); let mut num_turns = 0; let mut cost_usd = 0.0; let mut all_messages = Vec::new(); while let Some(msg) = rx.recv().await { match msg { SDKMessage::Result { text, usage: u, num_turns: t, cost_usd: c, messages, .. } => { result_text = text; usage = u; num_turns = t; cost_usd = c; all_messages = messages; } SDKMessage::Assistant { message, .. } => { let text = crate::types::extract_text(&message); if !text.is_empty() { result_text = text; } all_messages.push(message); } SDKMessage::Error { message } => { return Err(message); } _ => {} } } let _ = handle.await; let duration_ms = start.elapsed().as_millis() as u64; // Update agent's message history self.messages = all_messages.clone(); Ok(QueryResult { text: result_text, usage, num_turns, cost_usd, duration_ms, messages: all_messages, }) } /// Get current conversation messages. pub fn get_messages(&self) -> &[Message] { &self.messages } /// Clear conversation history. pub fn clear(&mut self) { self.messages.clear(); } /// Set the model. pub fn set_model(&mut self, model: &str) { self.api_client.set_model(model.to_string()); } /// Get the current model. pub fn model(&self) -> &str { self.api_client.model() } /// Get the cost tracker. pub fn cost_tracker(&self) -> &CostTracker { &self.cost_tracker } /// Get session ID. pub fn session_id(&self) -> &str { &self.session_id } /// Close the agent and clean up resources. pub async fn close(&self) { self.mcp_client.close_all().await; } /// Create a snapshot of the current registry for use in async context. fn registry_snapshot(&self) -> ToolRegistry { let mut snapshot = ToolRegistry::new(); for tool in self.registry.all() { snapshot.register(tool); } snapshot } }