1pub mod state;
5
6use std::sync::Arc;
7
8use state::ConversationState;
9use tokio::sync::mpsc;
10
11use crate::config::AgentConfig;
12use crate::error::{AgentError, Result};
13use crate::knowledge::KnowledgeBase;
14use crate::memory::MemoryManager;
15use crate::orchestrator::types::OrchestratorMessage;
16use crate::orchestrator::{create_orchestrator_async, OrchestratorConnection};
17use crate::planning::PlanManager;
18use crate::skills::SkillManager;
19use crate::tools::ToolRegistry;
20
21#[derive(Debug, Clone)]
23pub enum UserInterrupt {
24 NewMessage(String),
26 Cancel,
28 EditPlan(String),
30}
31
32pub struct AgentLoop {
37 config: AgentConfig,
38 cascade_config: Arc<llm_cascade::AppConfig>,
39 db_conn: tokio::sync::Mutex<rusqlite::Connection>,
40 state: ConversationState,
41 memory: MemoryManager,
42 #[allow(dead_code)]
43 skill_manager: SkillManager,
44 tool_registry: ToolRegistry,
45 #[allow(dead_code)]
46 knowledge: Option<Arc<KnowledgeBase>>,
47 orchestrator: Box<dyn OrchestratorConnection>,
48 interrupt_rx: mpsc::Receiver<UserInterrupt>,
49 interrupt_tx: mpsc::Sender<UserInterrupt>,
50 #[allow(dead_code)]
51 plan_manager: Arc<std::sync::Mutex<PlanManager>>,
52}
53
54impl AgentLoop {
55 pub async fn new(config: AgentConfig) -> Result<Self> {
57 let cascade_config =
59 llm_cascade::load_config(std::path::Path::new(&config.agent.cascade_config_path))
60 .map_err(|e| {
61 AgentError::ConfigError(format!("Failed to load cascade config: {}", e))
62 })?;
63
64 let cascade_config = Arc::new(cascade_config);
65
66 let db_path = &cascade_config.database.path;
68 let cascade_db_conn = llm_cascade::db::init_db(db_path)
69 .map_err(|e| AgentError::ConfigError(format!("Failed to init DB: {}", e)))?;
70
71 let system_prompt = if std::path::Path::new(&config.agent.soul_md_path).exists() {
73 std::fs::read_to_string(&config.agent.soul_md_path)?
74 } else {
75 "You are Cascade Agent, an autonomous AI assistant.".to_string()
76 };
77
78 let memory_db_conn = llm_cascade::db::init_db(db_path)
80 .map_err(|e| AgentError::ConfigError(format!("Failed to init DB for memory: {}", e)))?;
81 let memory =
82 MemoryManager::new(&config.memory, Arc::clone(&cascade_config), memory_db_conn)?;
83
84 let mut skill_manager =
86 SkillManager::new(std::path::PathBuf::from(&config.paths.skills_dir))?;
87 skill_manager.discover()?;
88
89 let tool_registry = ToolRegistry::new();
91
92 tool_registry.register(crate::tools::builtin::EchoTool);
94 tool_registry.register(crate::tools::builtin::ReadFileTool);
95 tool_registry.register(crate::tools::builtin::WriteFileTool);
96 tool_registry.register(crate::tools::builtin::AskUserTool);
97
98 if let Some(tavily) = crate::tools::search::TavilySearchTool::from_env(
100 &config.search.tavily_api_key_env,
101 config.search.max_results,
102 ) {
103 tool_registry.register(tavily);
104 }
105 if let Some(brave) = crate::tools::search::BraveSearchTool::from_env(
106 &config.search.brave_api_key_env,
107 config.search.max_results,
108 ) {
109 tool_registry.register(brave);
110 }
111
112 for skill_tool in skill_manager.all_tools() {
114 tool_registry.register_arc(std::sync::Arc::from(
115 skill_tool as Box<dyn crate::tools::Tool>,
116 ));
117 }
118
119 let plan_manager = Arc::new(std::sync::Mutex::new(PlanManager::new(
121 std::path::PathBuf::from(&config.paths.plans_dir),
122 )?));
123
124 let (interrupt_tx, interrupt_rx) = mpsc::channel::<UserInterrupt>(32);
125
126 let task_id = uuid::Uuid::new_v4().to_string();
127
128 tool_registry.register(crate::tools::planning_tools::CreatePlanTool::new(
130 Arc::clone(&plan_manager),
131 task_id.clone(),
132 ));
133 tool_registry.register(crate::tools::planning_tools::UpdatePlanStepTool::new(
134 Arc::clone(&plan_manager),
135 ));
136 tool_registry.register(crate::tools::planning_tools::ListPlansTool::new(
137 Arc::clone(&plan_manager),
138 ));
139 tool_registry.register(crate::tools::planning_tools::GetPlanTool::new(Arc::clone(
140 &plan_manager,
141 )));
142
143 tool_registry.register_list_tools();
145
146 let knowledge = match KnowledgeBase::new(&config.knowledge).await {
148 Ok(kb) => {
149 tracing::info!(target: "agent", "Knowledge base initialized (collection: {})", config.knowledge.default_collection);
150 Some(Arc::new(kb))
151 }
152 Err(e) => {
153 tracing::warn!(target: "agent", "Knowledge base init failed, running without it: {}", e);
154 None
155 }
156 };
157
158 if let Some(ref kb) = knowledge {
160 let kq_tool = crate::tools::knowledge_tool::KnowledgeQueryTool::with_defaults(
161 Arc::clone(kb) as Arc<dyn crate::tools::knowledge_tool::KnowledgeProvider>,
162 config.knowledge.default_collection.clone(),
163 config.knowledge.max_results,
164 );
165 tool_registry.register(kq_tool);
166 }
167
168 let orchestrator = create_orchestrator_async(&config.orchestrator).await?;
170
171 let state = ConversationState::new(system_prompt, task_id);
172
173 Ok(Self {
174 config,
175 cascade_config,
176 db_conn: tokio::sync::Mutex::new(cascade_db_conn),
177 state,
178 memory,
179 skill_manager,
180 tool_registry,
181 knowledge,
182 orchestrator,
183 interrupt_rx,
184 interrupt_tx,
185 plan_manager,
186 })
187 }
188
189 pub fn interrupt_sender(&self) -> mpsc::Sender<UserInterrupt> {
191 self.interrupt_tx.clone()
192 }
193
194 pub async fn run(&mut self, initial_prompt: String) -> Result<String> {
198 self.state.add_user_message(initial_prompt.clone());
199 self.orchestrator
200 .push(OrchestratorMessage::TaskStarted {
201 task_id: self.state.task_id.clone(),
202 description: initial_prompt,
203 })
204 .await;
205
206 let result = self.run_loop().await;
207
208 match &result {
209 Ok(_output) => {
210 self.orchestrator
211 .push(OrchestratorMessage::TaskCompleted {
212 task_id: self.state.task_id.clone(),
213 output_path: None,
214 })
215 .await;
216 }
217 Err(e) => {
218 self.orchestrator
219 .push(OrchestratorMessage::Error(e.to_string()))
220 .await;
221 }
222 }
223
224 result
225 }
226
227 async fn run_loop(&mut self) -> Result<String> {
229 loop {
230 while let Ok(interrupt) = self.interrupt_rx.try_recv() {
232 match interrupt {
233 UserInterrupt::NewMessage(msg) => {
234 self.state.add_user_message(msg);
235 }
236 UserInterrupt::Cancel => {
237 self.orchestrator
238 .push(OrchestratorMessage::TaskCancelled)
239 .await;
240 return Ok("Task cancelled by user.".into());
241 }
242 UserInterrupt::EditPlan(content) => {
243 self.state
244 .add_user_message(format!("[Plan Edit]: {}", content));
245 }
246 }
247 }
248
249 let token_count = self.memory.count_tokens(&self.state);
251 if self.memory.should_compact(token_count) {
252 match self.memory.compact(&mut self.state).await {
253 Ok(report) => {
254 self.orchestrator
255 .push(OrchestratorMessage::ContextCompacted {
256 before: report.tokens_before,
257 after: report.tokens_after,
258 })
259 .await;
260 tracing::info!(
261 target: "agent",
262 "Context compacted: {} -> {} tokens ({} msgs -> {} msgs)",
263 report.tokens_before,
264 report.tokens_after,
265 report.messages_before,
266 report.messages_after
267 );
268 }
269 Err(e) => {
270 tracing::warn!(target: "agent", "Compaction failed: {}", e);
271 }
272 }
273 }
274
275 let tool_defs = self.tool_registry.tool_definitions();
277 let conversation = self.state.to_conversation().with_tools(tool_defs);
278
279 let cascade_name = self.config.agent.cascade_name.clone();
281 let config = Arc::clone(&self.cascade_config);
282
283 let cascade_future = async {
284 let conn_lock = self.db_conn.lock().await;
285 llm_cascade::run_cascade(&cascade_name, &conversation, &config, &conn_lock).await
286 };
287
288 let response = tokio::select! {
289 cascade_result = cascade_future => {
290 cascade_result
291 }
292 Some(orch_msg) = self.orchestrator.recv() => {
293 self.handle_orchestrator_message(orch_msg).await;
294 continue;
295 }
296 };
297
298 let response = match response {
299 Ok(r) => r,
300 Err(cascade_err) => {
301 let saved_path = self.state.to_json_file(&self.config.paths.outputs_dir)?;
302 tracing::error!(
303 target: "agent",
304 "All cascade providers failed: {}. State saved to: {:?}",
305 cascade_err.message,
306 saved_path
307 );
308 return Err(AgentError::InferenceFailed(cascade_err.message));
309 }
310 };
311
312 let mut has_tool_calls = false;
314
315 for block in &response.content {
316 match block {
317 llm_cascade::ContentBlock::Text { text } => {
318 if !text.is_empty() {
319 self.state.add_assistant_text(text.clone());
320 self.orchestrator
321 .push(OrchestratorMessage::AssistantText(text.clone()))
322 .await;
323 }
324 }
325 llm_cascade::ContentBlock::ToolCall {
326 id,
327 name,
328 arguments,
329 } => {
330 has_tool_calls = true;
331 let start = std::time::Instant::now();
332
333 let args: serde_json::Value = serde_json::from_str(arguments)
334 .unwrap_or(serde_json::json!({"raw": arguments}));
335
336 tracing::info!(
337 target: "agent",
338 "Executing tool: {} (id={})",
339 name,
340 id
341 );
342
343 let tool_result = if name == "ask_user" {
344 self.handle_ask_user(&args).await
345 } else {
346 self.tool_registry.execute(name, args.clone()).await
347 };
348 let duration_ms = start.elapsed().as_millis() as u64;
349
350 let result_str = match &tool_result {
351 Ok(r) => r.to_json_string(),
352 Err(e) => serde_json::json!({
353 "status": "error",
354 "error": e.to_string()
355 })
356 .to_string(),
357 };
358
359 self.state.add_tool_result(id, &result_str);
360
361 let status_str = match &tool_result {
362 Ok(r) => r.status_str(),
363 Err(_) => "error",
364 };
365
366 self.orchestrator
367 .push(OrchestratorMessage::ToolExecuted {
368 tool: name.clone(),
369 status: status_str.to_string(),
370 duration_ms,
371 })
372 .await;
373
374 if (name == "tavily_search" || name == "brave_search")
376 && tool_result.is_ok()
377 {
378 self.store_search_results(name, &args, &tool_result).await;
379 }
380 }
381 }
382 }
383
384 if !has_tool_calls {
386 break;
387 }
388 if self.state.turn_count >= self.config.agent.max_tool_rounds {
389 self.orchestrator
390 .push(OrchestratorMessage::Warning(format!(
391 "Max tool rounds ({}) reached",
392 self.config.agent.max_tool_rounds
393 )))
394 .await;
395 break;
396 }
397 }
398
399 Ok(self.state.last_assistant_text().unwrap_or_default())
400 }
401
402 async fn handle_orchestrator_message(&mut self, msg: OrchestratorMessage) {
404 match msg {
405 OrchestratorMessage::UserReply { content } => {
406 self.state.add_user_message(content);
407 }
408 OrchestratorMessage::PlanApproval { approved, feedback } => {
409 let feedback_text = feedback.unwrap_or_default();
410 let approval_msg = if approved {
411 format!("[Plan Approved] {}", feedback_text)
412 } else {
413 format!("[Plan Rejected] {}", feedback_text)
414 };
415 self.state.add_user_message(approval_msg);
416 }
417 OrchestratorMessage::CancelTask => {
418 self.orchestrator
419 .push(OrchestratorMessage::TaskCancelled)
420 .await;
421 }
422 _ => {
423 tracing::debug!(target: "agent", "Ignoring orchestrator message: {:?}", msg);
424 }
425 }
426 }
427
428 async fn handle_ask_user(
430 &mut self,
431 args: &serde_json::Value,
432 ) -> crate::error::Result<crate::tools::ToolResult> {
433 let question = args
434 .get("question")
435 .and_then(|v| v.as_str())
436 .unwrap_or("(no question provided)");
437
438 if !self.orchestrator.is_connected() {
439 return Ok(crate::tools::ToolResult::ok(serde_json::json!({
440 "status": "no_orchestrator",
441 "question": question,
442 "answer": "No orchestrator connected. Use the interrupt channel to reply.",
443 })));
444 }
445
446 self.orchestrator
447 .push(OrchestratorMessage::UserQuestion {
448 question: question.to_string(),
449 })
450 .await;
451
452 tracing::info!(target: "agent", "Waiting for user reply to: {}", question);
453
454 match tokio::time::timeout(
455 std::time::Duration::from_secs(300),
456 self.orchestrator.recv(),
457 )
458 .await
459 {
460 Ok(Some(OrchestratorMessage::UserReply { content })) => {
461 Ok(crate::tools::ToolResult::ok(serde_json::json!({
462 "status": "replied",
463 "question": question,
464 "answer": content,
465 })))
466 }
467 Ok(Some(other)) => {
468 self.handle_orchestrator_message(other).await;
469 Ok(crate::tools::ToolResult::ok(serde_json::json!({
470 "status": "interrupted",
471 "question": question,
472 "answer": "User sent a different message instead of replying.",
473 })))
474 }
475 Ok(None) => Ok(crate::tools::ToolResult::ok(serde_json::json!({
476 "status": "timeout",
477 "question": question,
478 "answer": "Orchestrator disconnected before user replied.",
479 }))),
480 Err(_) => Ok(crate::tools::ToolResult::ok(serde_json::json!({
481 "status": "timeout",
482 "question": question,
483 "answer": "Timed out waiting for user reply (5 minutes).",
484 }))),
485 }
486 }
487
488 async fn store_search_results(
490 &self,
491 tool_name: &str,
492 args: &serde_json::Value,
493 tool_result: &crate::error::Result<crate::tools::ToolResult>,
494 ) {
495 let kb = match &self.knowledge {
496 Some(kb) => kb,
497 None => return,
498 };
499
500 let query = match args.get("query").and_then(|v| v.as_str()) {
501 Some(q) => q.to_string(),
502 None => return,
503 };
504
505 let results = match tool_result {
506 Ok(r) => &r.data,
507 Err(_) => return,
508 };
509
510 let result_array = match results.get("results").and_then(|v| v.as_array()) {
511 Some(arr) => arr,
512 None => return,
513 };
514
515 let entries: Vec<crate::knowledge::vectordb::KnowledgeEntry> = result_array
516 .iter()
517 .filter_map(|item| {
518 let text = format!(
519 "{}\n{}",
520 item.get("title").and_then(|v| v.as_str()).unwrap_or(""),
521 item.get("snippet").and_then(|v| v.as_str()).unwrap_or("")
522 );
523 if text.trim().is_empty() {
524 return None;
525 }
526 Some(crate::knowledge::vectordb::KnowledgeEntry {
527 text,
528 source: tool_name.to_string(),
529 metadata: serde_json::json!({
530 "url": item.get("url").and_then(|v| v.as_str()).unwrap_or(""),
531 "query": query,
532 }),
533 timestamp: chrono::Utc::now().timestamp(),
534 })
535 })
536 .collect();
537
538 if entries.is_empty() {
539 return;
540 }
541
542 let collection = &self.config.knowledge.default_collection;
543 match kb.store_results(collection, entries).await {
544 Ok(()) => {
545 tracing::info!(
546 target: "agent",
547 "Stored {} search results in knowledge base (collection: {})",
548 result_array.len(),
549 collection
550 );
551 }
552 Err(e) => {
553 tracing::warn!(
554 target: "agent",
555 "Failed to store search results in knowledge base: {}",
556 e
557 );
558 }
559 }
560 }
561
562 pub fn set_system_prompt(&mut self, prompt: String) {
564 self.state.system_prompt = prompt;
565 }
566
567 pub fn state(&self) -> &ConversationState {
569 &self.state
570 }
571}