1use crate::error::CliError;
2use aether_core::agent_spec::AgentSpec;
3use aether_core::core::{AgentBuilder, AgentHandle, Prompt};
4use aether_core::events::{AgentMessage, UserMessage};
5use aether_core::mcp::McpBuilder;
6use aether_core::mcp::McpSpawnResult;
7use aether_core::mcp::mcp;
8use aether_core::mcp::run_mcp_task::McpCommand;
9use aether_project::load_agent_catalog;
10use llm::{ChatMessage, LlmModel, ToolDefinition};
11use mcp_servers::McpBuilderExt;
12use mcp_utils::client::oauth::OAuthHandler;
13use mcp_utils::client::{ElicitationRequest, McpServerConfig};
14use mcp_utils::status::McpServerStatusEntry;
15use std::path::{Path, PathBuf};
16use tokio::sync::mpsc::{Receiver, Sender};
17use tokio::task::JoinHandle;
18use tracing::debug;
19
20pub struct RuntimeBuilder {
21 cwd: PathBuf,
22 spec: AgentSpec,
23 mcp_config: Option<PathBuf>,
24 extra_mcp_servers: Vec<McpServerConfig>,
25 oauth_applicator: Option<Box<dyn FnOnce(McpBuilder) -> McpBuilder + Send>>,
26 prompt_cache_key: Option<String>,
27}
28
29pub struct Runtime {
30 pub agent_tx: Sender<UserMessage>,
31 pub agent_rx: Receiver<AgentMessage>,
32 pub agent_handle: AgentHandle,
33 pub mcp_tx: Sender<McpCommand>,
34 pub elicitation_rx: Receiver<ElicitationRequest>,
35 pub server_statuses: Vec<McpServerStatusEntry>,
36 pub mcp_handle: JoinHandle<()>,
37}
38
39pub struct PromptInfo {
40 pub spec: AgentSpec,
41 pub tool_definitions: Vec<ToolDefinition>,
42}
43
44impl RuntimeBuilder {
45 pub fn new(cwd: &Path, model: &str) -> Result<Self, CliError> {
46 let cwd = cwd.canonicalize().map_err(CliError::IoError)?;
47 let parsed_model: LlmModel = model.parse().map_err(|e: String| CliError::ModelError(e))?;
48 let catalog = load_agent_catalog(&cwd).map_err(|e| CliError::AgentError(e.to_string()))?;
49 let spec = catalog.resolve_default(&parsed_model, None, &cwd);
50
51 Ok(Self {
52 cwd,
53 spec,
54 mcp_config: None,
55 extra_mcp_servers: Vec::new(),
56 oauth_applicator: None,
57 prompt_cache_key: None,
58 })
59 }
60
61 pub fn from_spec(cwd: PathBuf, spec: AgentSpec) -> Self {
62 Self {
63 cwd,
64 spec,
65 mcp_config: None,
66 extra_mcp_servers: Vec::new(),
67 oauth_applicator: None,
68 prompt_cache_key: None,
69 }
70 }
71
72 pub fn prompt_cache_key(mut self, key: String) -> Self {
73 self.prompt_cache_key = Some(key);
74 self
75 }
76
77 pub fn mcp_config(mut self, path: PathBuf) -> Self {
78 self.mcp_config = Some(path);
79 self
80 }
81
82 pub fn mcp_config_opt(self, path: Option<PathBuf>) -> Self {
83 match path {
84 Some(p) => self.mcp_config(p),
85 None => self,
86 }
87 }
88
89 pub fn extra_servers(mut self, servers: Vec<McpServerConfig>) -> Self {
90 self.extra_mcp_servers = servers;
91 self
92 }
93
94 pub fn oauth_handler<H: OAuthHandler + 'static>(mut self, handler: H) -> Self {
95 self.oauth_applicator = Some(Box::new(|builder| builder.with_oauth_handler(handler)));
96 self
97 }
98
99 pub async fn build(
100 self,
101 custom_prompt: Option<Prompt>,
102 messages: Option<Vec<ChatMessage>>,
103 ) -> Result<Runtime, CliError> {
104 let prompt_cache_key = self.prompt_cache_key.clone();
105 let mcp = self.spawn_mcp().await?;
106
107 let filtered_tools = mcp.spec.tools.apply(mcp.tool_definitions);
108 let mut agent_builder = AgentBuilder::from_spec(&mcp.spec, vec![])
109 .await
110 .map_err(|e| CliError::AgentError(e.to_string()))?
111 .tools(mcp.mcp_tx.clone(), filtered_tools);
112
113 if let Some(key) = prompt_cache_key {
114 agent_builder = agent_builder.prompt_cache_key(key);
115 }
116
117 if let Some(prompt) = custom_prompt {
118 agent_builder = agent_builder.system_prompt(prompt);
119 }
120
121 if let Some(msgs) = messages {
122 agent_builder = agent_builder.messages(msgs);
123 }
124
125 let (agent_tx, agent_rx, agent_handle) =
126 agent_builder.spawn().await.map_err(|e| CliError::AgentError(e.to_string()))?;
127
128 Ok(Runtime {
129 agent_tx,
130 agent_rx,
131 agent_handle,
132 mcp_tx: mcp.mcp_tx,
133 elicitation_rx: mcp.elicitation_rx,
134 server_statuses: mcp.server_statuses,
135 mcp_handle: mcp.mcp_handle,
136 })
137 }
138
139 pub async fn build_prompt_info(self) -> Result<PromptInfo, CliError> {
140 let mcp = self.spawn_mcp().await?;
141 let filtered_tools = mcp.spec.tools.apply(mcp.tool_definitions);
142 Ok(PromptInfo { spec: mcp.spec, tool_definitions: filtered_tools })
143 }
144
145 async fn spawn_mcp(self) -> Result<McpParts, CliError> {
146 let mut builder = mcp().with_builtin_servers(self.cwd.clone(), &self.cwd);
147
148 if !self.extra_mcp_servers.is_empty() {
149 builder = builder.with_servers(self.extra_mcp_servers);
150 }
151
152 if let Some(apply_oauth) = self.oauth_applicator {
153 builder = apply_oauth(builder);
154 }
155
156 let mcp_config_path = self.mcp_config.or(self.spec.mcp_config_path.clone());
157
158 if let Some(ref config_path) = mcp_config_path {
159 debug!("Loading MCP config from: {}", config_path.display());
160 let config_str =
161 config_path.to_str().ok_or_else(|| CliError::McpError("Invalid MCP config path".to_string()))?;
162
163 builder = builder.from_json_file(config_str).await.map_err(|e| CliError::McpError(e.to_string()))?;
164 }
165
166 let McpSpawnResult {
167 tool_definitions,
168 instructions,
169 server_statuses,
170 command_tx: mcp_tx,
171 elicitation_rx,
172 handle: mcp_handle,
173 } = builder.spawn().await.map_err(|e| CliError::McpError(e.to_string()))?;
174
175 let mut spec = self.spec;
176 spec.prompts.push(Prompt::mcp_instructions(instructions));
177
178 Ok(McpParts { spec, tool_definitions, mcp_tx, elicitation_rx, server_statuses, mcp_handle })
179 }
180}
181
182struct McpParts {
183 spec: AgentSpec,
184 tool_definitions: Vec<ToolDefinition>,
185 mcp_tx: Sender<McpCommand>,
186 elicitation_rx: Receiver<ElicitationRequest>,
187 server_statuses: Vec<McpServerStatusEntry>,
188 mcp_handle: JoinHandle<()>,
189}