1use super::agent::{AgentConfig, AutoContinue, RetryConfig};
2use crate::agent_spec::AgentSpec;
3use crate::context::CompactionConfig;
4use crate::core::{Agent, AgentDeps, Prompt, PromptCache, Result};
5use crate::events::{AgentEvent, AgentObserver, Command};
6use crate::mcp::run_mcp_task::McpCommand;
7use llm::parser::ModelProviderParser;
8use llm::types::IsoString;
9use llm::{ChatMessage, Context, ModelSettings, StreamingModelProvider, ToolDefinition};
10use std::sync::Arc;
11use std::time::Duration;
12use tokio::sync::mpsc::{self, Receiver, Sender};
13use tokio::task::JoinHandle;
14
15pub struct AgentHandle {
17 handle: JoinHandle<()>,
18}
19
20impl AgentHandle {
21 pub fn abort(&self) {
23 self.handle.abort();
24 }
25
26 pub fn is_finished(&self) -> bool {
28 self.handle.is_finished()
29 }
30
31 pub async fn await_completion(self) {
33 let _ = self.handle.await;
34 }
35}
36
37pub struct AgentBuilder {
38 llm: Arc<dyn StreamingModelProvider>,
39 prompts: Vec<Prompt>,
40 tool_definitions: Vec<ToolDefinition>,
41 initial_messages: Vec<ChatMessage>,
42 mcp_tx: Option<Sender<McpCommand>>,
43 channel_capacity: usize,
44 tool_timeout: Duration,
45 compaction_config: Option<CompactionConfig>,
46 max_auto_continues: u32,
47 retry_config: RetryConfig,
48 context_window: Option<u32>,
49 model_settings: ModelSettings,
50 observers: Vec<Box<dyn AgentObserver>>,
51}
52
53impl AgentBuilder {
54 pub fn new(llm: Arc<dyn StreamingModelProvider>) -> Self {
55 Self {
56 llm,
57 prompts: Vec::new(),
58 tool_definitions: Vec::new(),
59 initial_messages: Vec::new(),
60 mcp_tx: None,
61 channel_capacity: 1000,
62 tool_timeout: Duration::from_mins(20),
63 compaction_config: Some(CompactionConfig::default()),
64 max_auto_continues: 3,
65 retry_config: RetryConfig::default(),
66 context_window: None,
67 model_settings: ModelSettings::default(),
68 observers: Vec::new(),
69 }
70 }
71
72 pub async fn from_spec(spec: &AgentSpec, base_prompts: Vec<Prompt>, deps: &AgentDeps) -> Result<Self> {
77 let parser = ModelProviderParser::default().with_provider_connections(spec.provider_connections.clone());
78 let parser = match deps.oauth_credential_store.clone() {
79 Some(store) => parser.with_codex_provider(store),
80 None => parser,
81 };
82 let (provider, _) = parser.parse(&spec.model).await?;
83 let mut builder = Self::new(Arc::from(provider))
84 .context_window(spec.context_window)
85 .model_settings(spec.model_settings.clone());
86
87 if let Some(observer) = deps.observer() {
88 builder = builder.observer(observer);
89 }
90
91 for prompt in base_prompts {
92 builder = builder.system_prompt(prompt);
93 }
94
95 for prompt in &spec.prompts {
96 builder = builder.system_prompt(prompt.clone());
97 }
98
99 Ok(builder)
100 }
101
102 pub fn system_prompt(mut self, prompt: Prompt) -> Self {
106 self.prompts.push(prompt);
107 self
108 }
109
110 pub fn tools(mut self, tx: Sender<McpCommand>, tools: Vec<ToolDefinition>) -> Self {
111 self.tool_definitions = tools;
112 self.mcp_tx = Some(tx);
113 self
114 }
115
116 pub fn tool_timeout(mut self, timeout: Duration) -> Self {
123 self.tool_timeout = timeout;
124 self
125 }
126
127 pub fn compaction(mut self, config: CompactionConfig) -> Self {
148 self.compaction_config = Some(config);
149 self
150 }
151
152 pub fn disable_compaction(mut self) -> Self {
156 self.compaction_config = None;
157 self
158 }
159
160 pub fn max_auto_continues(mut self, max: u32) -> Self {
180 self.max_auto_continues = max;
181 self
182 }
183
184 pub fn retry(mut self, config: RetryConfig) -> Self {
186 self.retry_config = config;
187 self
188 }
189
190 pub fn context_window(mut self, context_window: Option<u32>) -> Self {
192 self.context_window = context_window;
193 self
194 }
195
196 pub fn model_settings(mut self, model_settings: ModelSettings) -> Self {
199 self.model_settings = model_settings;
200 self
201 }
202
203 pub fn messages(mut self, messages: Vec<ChatMessage>) -> Self {
207 self.initial_messages = messages;
208 self
209 }
210
211 pub fn observer(mut self, observer: Box<dyn AgentObserver>) -> Self {
213 self.observers.push(observer);
214 self
215 }
216
217 pub async fn spawn(self) -> Result<(Sender<Command>, Receiver<AgentEvent>, AgentHandle)> {
218 let mut prompt_cache = PromptCache::new(self.prompts);
219 let system_content = prompt_cache.render().await?;
220 let mut messages = Vec::new();
221
222 if !system_content.is_empty() {
223 messages.push(ChatMessage::System { content: system_content, timestamp: IsoString::now() });
224 }
225
226 messages.extend(self.initial_messages);
227 let (command_tx, command_rx) = mpsc::channel::<Command>(self.channel_capacity);
228 let (message_tx, agent_event_rx) = mpsc::channel::<AgentEvent>(self.channel_capacity);
229 let mut context = Context::new(messages, self.tool_definitions);
230 context.set_model_settings(self.model_settings);
231
232 let config = AgentConfig {
233 llm: self.llm,
234 context,
235 mcp_command_tx: self.mcp_tx,
236 tool_timeout: self.tool_timeout,
237 compaction_config: self.compaction_config,
238 auto_continue: AutoContinue::new(self.max_auto_continues),
239 retry_config: self.retry_config,
240 context_window: self.context_window,
241 prompt_cache,
242 observers: self.observers,
243 };
244
245 let agent = Agent::new(config, command_rx, message_tx);
246 let agent_handle = tokio::spawn(agent.run());
247
248 Ok((command_tx, agent_event_rx, AgentHandle { handle: agent_handle }))
249 }
250}
251
252#[cfg(test)]
253mod tests {
254 use super::*;
255 use crate::agent_spec::{AgentSpecExposure, ToolFilter};
256 use llm::ProviderConnectionOverrides;
257
258 #[tokio::test]
259 async fn test_agent_handle_is_finished() {
260 let handle = AgentHandle { handle: tokio::spawn(async {}) };
261 handle.await_completion().await;
262 }
263
264 #[tokio::test]
265 async fn test_agent_handle_abort() {
266 let handle = AgentHandle { handle: tokio::spawn(std::future::pending::<()>()) };
267 assert!(!handle.is_finished());
268 handle.abort();
269 while !handle.is_finished() {
270 tokio::task::yield_now().await;
271 }
272 }
273
274 #[tokio::test]
275 async fn system_prompt_preserves_add_order() {
276 let builder = AgentBuilder::new(Arc::new(llm::testing::FakeLlmProvider::new(vec![])))
277 .system_prompt(Prompt::text("first"))
278 .system_prompt(Prompt::text("second"))
279 .system_prompt(Prompt::text("third"));
280
281 let rendered = Prompt::build_all(&builder.prompts).await.unwrap();
282
283 assert_eq!(rendered, "first\n\nsecond\n\nthird");
284 }
285
286 #[tokio::test]
287 async fn from_spec_applies_context_window_and_model_settings() {
288 let settings = ModelSettings { temperature: Some(0.0), max_tokens: Some(128), ..Default::default() };
289 let spec = AgentSpec {
290 name: "alloy".to_string(),
291 description: "alloy".to_string(),
292 model: "ollama:llama3.2,llamacpp:local".to_string(),
293 reasoning_effort: None,
294 model_settings: settings.clone(),
295 context_window: Some(200_000),
296 prompts: vec![],
297 provider_connections: ProviderConnectionOverrides::default(),
298 mcp_config_sources: Vec::new(),
299 exposure: AgentSpecExposure::both(),
300 tools: ToolFilter::default(),
301 };
302
303 let builder = AgentBuilder::from_spec(&spec, vec![], &AgentDeps::default()).await.unwrap();
304
305 assert_eq!(builder.context_window, Some(200_000));
306 assert_eq!(builder.model_settings, settings);
307 }
308
309 #[tokio::test]
310 async fn from_spec_accepts_alloy_model_specs() {
311 let spec = AgentSpec {
312 name: "alloy".to_string(),
313 description: "alloy".to_string(),
314 model: "ollama:llama3.2,llamacpp:local".to_string(),
315 reasoning_effort: None,
316 model_settings: ModelSettings::default(),
317 context_window: None,
318 prompts: vec![],
319 provider_connections: ProviderConnectionOverrides::default(),
320 mcp_config_sources: Vec::new(),
321 exposure: AgentSpecExposure::both(),
322 tools: ToolFilter::default(),
323 };
324
325 let builder = AgentBuilder::from_spec(&spec, vec![], &AgentDeps::default()).await;
326 assert!(builder.is_ok());
327 }
328}