1use crate::error::CliError;
2use aether_core::agent_spec::{AgentSpec, McpConfigSource};
3use aether_core::core::{AgentBuilder, AgentDeps, AgentHandle, Prompt};
4use aether_core::events::{AgentEvent, Command};
5use aether_core::mcp::McpBuilder;
6use aether_core::mcp::McpSpawnResult;
7use aether_core::mcp::mcp;
8use aether_core::mcp::run_mcp_task::McpCommand;
9use llm::{ChatMessage, LlmModel, ToolDefinition};
10use mcp_servers::McpBuilderExt;
11use mcp_utils::client::{McpClientEvent, McpConnectionDetails, McpServer, OAuthHandlerFactory};
12use std::path::{Path, PathBuf};
13use tokio::sync::mpsc::{Receiver, Sender};
14use tokio::task::JoinHandle;
15use tracing::debug;
16
17pub struct RuntimeBuilder {
18 cwd: PathBuf,
19 spec: AgentSpec,
20 mcp_config_sources: Vec<McpConfigSource>,
21 extra_mcp_servers: Vec<McpServer>,
22 oauth_applicator: Option<Box<dyn FnOnce(McpBuilder) -> McpBuilder + Send>>,
23 agent_deps: AgentDeps,
24 prompt_cache_key: Option<String>,
25}
26
27pub struct Runtime {
28 pub agent_tx: Sender<Command>,
29 pub agent_rx: Receiver<AgentEvent>,
30 pub agent_handle: AgentHandle,
31 pub mcp_tx: Sender<McpCommand>,
32 pub event_rx: Receiver<McpClientEvent>,
33 pub mcp_handle: JoinHandle<()>,
34}
35
36pub struct PromptInfo {
37 pub spec: AgentSpec,
38 pub tool_definitions: Vec<ToolDefinition>,
39}
40
41impl RuntimeBuilder {
42 pub fn new(cwd: &Path, model: &str) -> Result<Self, CliError> {
43 let cwd = cwd.canonicalize().map_err(CliError::IoError)?;
44 let parsed_model: LlmModel = model.parse().map_err(CliError::ModelError)?;
45 let spec = AgentSpec::default_spec(&parsed_model, None, Vec::new());
46
47 Ok(Self {
48 cwd,
49 spec,
50 mcp_config_sources: Vec::new(),
51 extra_mcp_servers: Vec::new(),
52 oauth_applicator: None,
53 agent_deps: AgentDeps::default(),
54 prompt_cache_key: None,
55 })
56 }
57
58 pub fn from_spec(cwd: PathBuf, spec: AgentSpec) -> Self {
59 Self {
60 cwd,
61 spec,
62 mcp_config_sources: Vec::new(),
63 extra_mcp_servers: Vec::new(),
64 oauth_applicator: None,
65 agent_deps: AgentDeps::default(),
66 prompt_cache_key: None,
67 }
68 }
69
70 pub fn prompt_cache_key(mut self, key: String) -> Self {
71 self.prompt_cache_key = Some(key);
72 self
73 }
74
75 pub fn agent_deps(mut self, deps: AgentDeps) -> Self {
76 self.agent_deps = deps;
77 self
78 }
79
80 pub fn mcp_sources(mut self, sources: Vec<McpConfigSource>) -> Self {
83 self.mcp_config_sources = sources;
84 self
85 }
86
87 pub fn extra_servers(mut self, servers: Vec<McpServer>) -> Self {
88 self.extra_mcp_servers = servers;
89 self
90 }
91
92 pub fn oauth_handler_factory(mut self, factory: OAuthHandlerFactory) -> Self {
93 self.oauth_applicator = Some(Box::new(|builder| builder.with_oauth_handler_factory(factory)));
94 self
95 }
96
97 pub async fn build(
98 self,
99 custom_prompt: Option<Prompt>,
100 messages: Option<Vec<ChatMessage>>,
101 ) -> Result<Runtime, CliError> {
102 let deps = self.agent_deps.clone();
103 let prompt_cache_key = self.prompt_cache_key.clone();
104 let (spec, spawn) = self.spawn_mcp().await?;
105 let McpSpawnResult { command_tx: mcp_tx, event_rx, handle: mcp_handle } = spawn;
106
107 let (agent_tx, agent_rx, agent_handle) =
108 spawn_agent(&spec, &deps, mcp_tx.clone(), Vec::new(), prompt_cache_key, |mut agent_builder| {
109 if let Some(prompt) = custom_prompt {
110 agent_builder = agent_builder.system_prompt(prompt);
111 }
112 if let Some(msgs) = messages {
113 agent_builder = agent_builder.messages(msgs);
114 }
115 agent_builder
116 })
117 .await?;
118
119 Ok(Runtime { agent_tx, agent_rx, agent_handle, mcp_tx, event_rx, mcp_handle })
120 }
121
122 pub async fn build_ready(self, messages: Vec<ChatMessage>) -> Result<(Runtime, McpConnectionDetails), CliError> {
129 let deps = self.agent_deps.clone();
130 let prompt_cache_key = self.prompt_cache_key.clone();
131 let (spec, mut spawn) = self.spawn_mcp().await?;
132 let snapshot = spawn
133 .block_until_ready()
134 .await
135 .ok_or_else(|| CliError::McpError("MCP bootstrap aborted before completion".to_string()))?;
136 let McpSpawnResult { command_tx: mcp_tx, event_rx, handle: mcp_handle } = spawn;
137
138 let filtered_tools = spec.tools.apply(snapshot.tool_definitions.clone());
139 let mut runtime_spec = spec;
140 runtime_spec.prompts.push(Prompt::McpInstructions(snapshot.instructions.clone()));
141
142 let (agent_tx, agent_rx, agent_handle) =
143 spawn_agent(&runtime_spec, &deps, mcp_tx.clone(), filtered_tools, prompt_cache_key, |agent_builder| {
144 agent_builder.messages(messages)
145 })
146 .await?;
147
148 Ok((Runtime { agent_tx, agent_rx, agent_handle, mcp_tx, event_rx, mcp_handle }, snapshot))
149 }
150
151 pub async fn build_prompt_info(self) -> Result<PromptInfo, CliError> {
152 let (spec, mut spawn) = self.spawn_mcp().await?;
153 let details = spawn
154 .block_until_ready()
155 .await
156 .ok_or_else(|| CliError::McpError("MCP bootstrap aborted before completion".to_string()))?;
157 let filtered_tools = spec.tools.apply(details.tool_definitions);
158 Ok(PromptInfo { spec, tool_definitions: filtered_tools })
159 }
160
161 async fn spawn_mcp(self) -> Result<(AgentSpec, McpSpawnResult), CliError> {
162 let mut builder = mcp(&self.cwd).with_agent_deps(self.agent_deps.clone());
163
164 if let Some(apply_oauth) = self.oauth_applicator {
165 builder = apply_oauth(builder);
166 }
167
168 builder = builder.with_builtin_servers();
169
170 if !self.extra_mcp_servers.is_empty() {
171 builder = builder.with_servers(self.extra_mcp_servers);
172 }
173
174 let mcp_config_sources: Vec<McpConfigSource> = if self.mcp_config_sources.is_empty() {
175 self.spec.mcp_config_sources.clone()
176 } else {
177 self.mcp_config_sources
178 };
179
180 if !mcp_config_sources.is_empty() {
181 debug!("Loading MCP configs from: {:?}", mcp_config_sources);
182 builder = builder
183 .from_mcp_config_sources(&mcp_config_sources)
184 .await
185 .map_err(|e| CliError::McpError(e.to_string()))?;
186 }
187
188 let spawn = builder.spawn().await.map_err(|e| CliError::McpError(e.to_string()))?;
189 Ok((self.spec, spawn))
190 }
191}
192
193async fn spawn_agent(
194 spec: &AgentSpec,
195 deps: &AgentDeps,
196 mcp_tx: Sender<McpCommand>,
197 tool_definitions: Vec<ToolDefinition>,
198 prompt_cache_key: Option<String>,
199 configure: impl FnOnce(AgentBuilder) -> AgentBuilder,
200) -> Result<(Sender<Command>, Receiver<AgentEvent>, AgentHandle), CliError> {
201 let mut builder = AgentBuilder::from_spec(spec, vec![], deps)
202 .await
203 .map_err(|error| CliError::AgentError(error.to_string()))?
204 .tools(mcp_tx, tool_definitions);
205
206 if let Some(key) = prompt_cache_key {
207 builder = builder.prompt_cache_key(key);
208 }
209
210 configure(builder).spawn().await.map_err(|error| CliError::AgentError(error.to_string()))
211}