agent_core/agent/core.rs
1// AgentCore - Complete working agent out of the box
2//
3// Users call `AgentCore::new(config)` then `core.run()` and get a working chat agent.
4
5use std::io;
6use std::sync::Arc;
7
8use tokio::runtime::Runtime;
9use tokio::sync::mpsc;
10use tokio_util::sync::CancellationToken;
11
12use crate::controller::{
13 ControllerEvent, ControllerInputPayload, LLMController, LLMSessionConfig, LLMTool,
14 PermissionRegistry, ToolRegistry, UserInteractionRegistry,
15};
16
17use super::config::{load_config, AgentConfig, LLMRegistry};
18use super::error::AgentError;
19use super::logger::Logger;
20use super::messages::channels::DEFAULT_CHANNEL_SIZE;
21use super::messages::UiMessage;
22use super::router::InputRouter;
23
24use crate::tui::{App, AppConfig, DefaultKeyHandler, ExitHandler, KeyBindings, KeyHandler, LayoutTemplate, SessionInfo};
25use crate::tui::widgets::{Widget, ConversationView, ConversationViewFactory};
26
27/// Sender for messages from TUI to controller
28pub type ToControllerTx = mpsc::Sender<ControllerInputPayload>;
29/// Receiver for messages from TUI to controller
30pub type ToControllerRx = mpsc::Receiver<ControllerInputPayload>;
31/// Sender for messages from controller to TUI
32pub type FromControllerTx = mpsc::Sender<UiMessage>;
33/// Receiver for messages from controller to TUI
34pub type FromControllerRx = mpsc::Receiver<UiMessage>;
35
36/// AgentCore - A complete, working agent infrastructure.
37///
38/// AgentCore provides all the infrastructure needed for an LLM-powered agent:
39/// - Logging with tracing
40/// - LLM configuration loading
41/// - Tokio async runtime
42/// - LLMController for session management
43/// - Communication channels
44/// - User interaction and permission registries
45///
46/// # Basic Usage
47///
48/// ```ignore
49/// struct MyConfig;
50/// impl AgentConfig for MyConfig {
51/// fn config_path(&self) -> &str { ".myagent/config.yaml" }
52/// fn default_system_prompt(&self) -> &str { "You are helpful." }
53/// fn log_prefix(&self) -> &str { "myagent" }
54/// fn name(&self) -> &str { "MyAgent" }
55/// }
56///
57/// fn main() -> io::Result<()> {
58/// let mut core = AgentCore::new(&MyConfig)?;
59/// // Access channels and controller to wire up your TUI
60/// // then run your TUI loop
61/// Ok(())
62/// }
63/// ```
64pub struct AgentCore {
65 /// Logger instance (must be kept alive)
66 #[allow(dead_code)]
67 logger: Logger,
68
69 /// Agent name for display
70 name: String,
71
72 /// Agent version for display
73 version: String,
74
75 /// Factory for creating conversation views
76 conversation_factory: Option<ConversationViewFactory>,
77
78 /// Tokio runtime for async operations
79 runtime: Runtime,
80
81 /// The LLM controller
82 controller: Arc<LLMController>,
83
84 /// LLM provider registry (loaded from config)
85 llm_registry: Option<LLMRegistry>,
86
87 /// Sender for messages from TUI to controller
88 to_controller_tx: ToControllerTx,
89
90 /// Receiver for messages from TUI to controller (consumed by InputRouter)
91 to_controller_rx: Option<ToControllerRx>,
92
93 /// Sender for messages from controller to TUI (held by event handler)
94 #[allow(dead_code)]
95 from_controller_tx: FromControllerTx,
96
97 /// Receiver for messages from controller to TUI
98 from_controller_rx: Option<FromControllerRx>,
99
100 /// Cancellation token for graceful shutdown
101 cancel_token: CancellationToken,
102
103 /// User interaction registry for AskUserQuestions tool
104 user_interaction_registry: Arc<UserInteractionRegistry>,
105
106 /// Permission registry for AskForPermissions tool
107 permission_registry: Arc<PermissionRegistry>,
108
109 /// Tool definitions to register on sessions
110 tool_definitions: Vec<LLMTool>,
111
112 /// Widgets to register with the App
113 widgets_to_register: Vec<Box<dyn Widget>>,
114
115 /// Layout template for the TUI
116 layout_template: Option<LayoutTemplate>,
117
118 /// Key handler for customizable key bindings
119 key_handler: Option<Box<dyn KeyHandler>>,
120
121 /// Exit handler for cleanup before quitting
122 exit_handler: Option<Box<dyn ExitHandler>>,
123
124 /// Slash commands (None means use defaults)
125 commands: Option<Vec<Box<dyn crate::tui::commands::SlashCommand>>>,
126
127 /// Extension data available to commands
128 command_extension: Option<Box<dyn std::any::Any + Send>>,
129
130 /// Custom status bar widget (replaces default if provided)
131 custom_status_bar: Option<Box<dyn Widget>>,
132
133 /// Whether to hide the default status bar
134 hide_status_bar: bool,
135}
136
137impl AgentCore {
138 /// Create a new AgentCore with the given configuration.
139 ///
140 /// This initializes:
141 /// - Logging infrastructure
142 /// - LLM configuration from config file or environment
143 /// - Tokio runtime
144 /// - Communication channels
145 /// - LLMController
146 /// - User interaction and permission registries
147 pub fn new<C: AgentConfig>(config: &C) -> io::Result<Self> {
148 let logger = Logger::new(config.log_prefix())?;
149 tracing::info!("{} agent initialized", config.name());
150
151 // Load LLM configuration
152 let llm_registry = load_config(config);
153 if llm_registry.is_empty() {
154 tracing::warn!(
155 "No LLM providers configured. Set ANTHROPIC_API_KEY or create ~/{}",
156 config.config_path()
157 );
158 } else {
159 tracing::info!(
160 "Loaded {} LLM provider(s): {:?}",
161 llm_registry.providers().len(),
162 llm_registry.providers()
163 );
164 }
165
166 // Create tokio runtime for async operations
167 let runtime = Runtime::new().map_err(|e| {
168 io::Error::new(
169 io::ErrorKind::Other,
170 format!("Failed to create runtime: {}", e),
171 )
172 })?;
173
174 // Create communication channels
175 let (to_controller_tx, to_controller_rx) =
176 mpsc::channel::<ControllerInputPayload>(DEFAULT_CHANNEL_SIZE);
177 let (from_controller_tx, from_controller_rx) =
178 mpsc::channel::<UiMessage>(DEFAULT_CHANNEL_SIZE);
179
180 // Create the controller with an event handler that forwards to the UI channel
181 let ui_tx = from_controller_tx.clone();
182 let event_handler = Box::new(move |event: ControllerEvent| {
183 let msg = convert_controller_event_to_ui_message(event);
184 // Try to send, log if channel is full (non-blocking to avoid deadlock)
185 if let Err(e) = ui_tx.try_send(msg) {
186 tracing::warn!("Failed to send controller event to UI: {}", e);
187 }
188 });
189
190 let controller = Arc::new(LLMController::new(Some(event_handler)));
191 let cancel_token = CancellationToken::new();
192
193 // Create channel for user interaction events
194 let (interaction_event_tx, mut interaction_event_rx) =
195 mpsc::channel::<ControllerEvent>(DEFAULT_CHANNEL_SIZE);
196
197 // Create the user interaction registry
198 let user_interaction_registry =
199 Arc::new(UserInteractionRegistry::new(interaction_event_tx));
200
201 // Spawn a task to forward user interaction events to the UI channel
202 let ui_tx_for_interactions = from_controller_tx.clone();
203 runtime.spawn(async move {
204 while let Some(event) = interaction_event_rx.recv().await {
205 let msg = convert_controller_event_to_ui_message(event);
206 if let Err(e) = ui_tx_for_interactions.try_send(msg) {
207 tracing::warn!("Failed to send user interaction event to UI: {}", e);
208 }
209 }
210 });
211
212 // Create channel for permission events
213 let (permission_event_tx, mut permission_event_rx) =
214 mpsc::channel::<ControllerEvent>(DEFAULT_CHANNEL_SIZE);
215
216 // Create the permission registry
217 let permission_registry = Arc::new(PermissionRegistry::new(permission_event_tx));
218
219 // Spawn a task to forward permission events to the UI channel
220 let ui_tx_for_permissions = from_controller_tx.clone();
221 runtime.spawn(async move {
222 while let Some(event) = permission_event_rx.recv().await {
223 let msg = convert_controller_event_to_ui_message(event);
224 if let Err(e) = ui_tx_for_permissions.try_send(msg) {
225 tracing::warn!("Failed to send permission event to UI: {}", e);
226 }
227 }
228 });
229
230 Ok(Self {
231 logger,
232 name: config.name().to_string(),
233 version: "0.1.0".to_string(),
234 conversation_factory: None,
235 runtime,
236 controller,
237 llm_registry: Some(llm_registry),
238 to_controller_tx,
239 to_controller_rx: Some(to_controller_rx),
240 from_controller_tx,
241 from_controller_rx: Some(from_controller_rx),
242 cancel_token,
243 user_interaction_registry,
244 permission_registry,
245 tool_definitions: Vec::new(),
246 widgets_to_register: Vec::new(),
247 layout_template: None,
248 key_handler: None,
249 exit_handler: None,
250 commands: None,
251 command_extension: None,
252 custom_status_bar: None,
253 hide_status_bar: false,
254 })
255 }
256
257 /// Set the agent version for display.
258 pub fn set_version(&mut self, version: impl Into<String>) {
259 self.version = version.into();
260 }
261
262 /// Set the conversation view factory.
263 ///
264 /// The factory is called to create conversation views when sessions
265 /// are created or cleared. This allows customizing the chat view
266 /// with custom welcome screens, title renderers, etc.
267 ///
268 /// # Example
269 ///
270 /// ```ignore
271 /// agent.set_conversation_factory(|| {
272 /// Box::new(ChatView::new()
273 /// .with_title("My Agent")
274 /// .with_initial_content(welcome_renderer))
275 /// });
276 /// ```
277 pub fn set_conversation_factory<F>(&mut self, factory: F) -> &mut Self
278 where
279 F: Fn() -> Box<dyn ConversationView> + Send + Sync + 'static,
280 {
281 self.conversation_factory = Some(Box::new(factory));
282 self
283 }
284
285 /// Set the layout template for the TUI.
286 ///
287 /// This allows customizing how widgets are arranged in the terminal.
288 /// If not set, the default Standard layout with panels is used.
289 ///
290 /// # Example
291 ///
292 /// ```ignore
293 /// // Use standard layout (default)
294 /// agent.set_layout(LayoutTemplate::standard());
295 ///
296 /// // Add a sidebar
297 /// agent.set_layout(LayoutTemplate::with_sidebar("file_browser", 30));
298 ///
299 /// // Minimal layout (no status bar)
300 /// agent.set_layout(LayoutTemplate::minimal());
301 /// ```
302 pub fn set_layout(&mut self, template: LayoutTemplate) -> &mut Self {
303 self.layout_template = Some(template);
304 self
305 }
306
307 /// Set a custom key handler for the TUI.
308 ///
309 /// This allows full control over key handling behavior. For simpler
310 /// customization where you just want to change which keys trigger
311 /// which actions, use [`Self::set_key_bindings`] instead.
312 ///
313 /// # Example
314 ///
315 /// ```ignore
316 /// struct VimKeyHandler { mode: VimMode }
317 /// impl KeyHandler for VimKeyHandler {
318 /// fn handle_key(&mut self, key: KeyEvent, ctx: &KeyContext) -> AppKeyResult {
319 /// // Implement vim-style modal editing
320 /// }
321 /// }
322 ///
323 /// let mut agent = AgentCore::new(&config)?;
324 /// agent.set_key_handler(VimKeyHandler { mode: VimMode::Normal });
325 /// ```
326 pub fn set_key_handler<H: KeyHandler>(&mut self, handler: H) -> &mut Self {
327 self.key_handler = Some(Box::new(handler));
328 self
329 }
330
331 /// Set custom key bindings using the default handler.
332 ///
333 /// This is a simpler alternative to [`Self::set_key_handler`] when you
334 /// only need to change which keys trigger which actions.
335 ///
336 /// # Example
337 ///
338 /// ```ignore
339 /// // Use minimal bindings (Esc to quit, arrow keys only)
340 /// let mut agent = AgentCore::new(&config)?;
341 /// agent.set_key_bindings(KeyBindings::minimal());
342 ///
343 /// // Or customize specific bindings
344 /// let mut bindings = KeyBindings::emacs();
345 /// bindings.quit = vec![KeyCombo::key(KeyCode::Esc)];
346 /// agent.set_key_bindings(bindings);
347 /// ```
348 pub fn set_key_bindings(&mut self, bindings: KeyBindings) -> &mut Self {
349 self.key_handler = Some(Box::new(DefaultKeyHandler::new(bindings)));
350 self
351 }
352
353 /// Set an exit handler for cleanup before quitting.
354 ///
355 /// The exit handler's `on_exit()` method is called when the user
356 /// confirms exit. If it returns `false`, the exit is cancelled.
357 ///
358 /// # Example
359 ///
360 /// ```ignore
361 /// struct SaveOnExitHandler { session_file: PathBuf }
362 /// impl ExitHandler for SaveOnExitHandler {
363 /// fn on_exit(&mut self) -> bool {
364 /// self.save_session();
365 /// true // proceed with exit
366 /// }
367 /// }
368 ///
369 /// let mut agent = AgentCore::new(&config)?;
370 /// agent.set_exit_handler(SaveOnExitHandler { session_file: path });
371 /// ```
372 pub fn set_exit_handler<H: ExitHandler>(&mut self, handler: H) -> &mut Self {
373 self.exit_handler = Some(Box::new(handler));
374 self
375 }
376
377 /// Set the slash commands for this agent.
378 ///
379 /// If not called, uses the default command set.
380 ///
381 /// # Example
382 ///
383 /// ```ignore
384 /// use agent_core::tui::commands::{CommandRegistry, CustomCommand, CommandResult};
385 ///
386 /// agent.set_commands(
387 /// CommandRegistry::with_defaults()
388 /// .add(CustomCommand::new("deploy", "Deploy app", |args, ctx| {
389 /// CommandResult::Message(format!("Deployed to {}", args))
390 /// }))
391 /// .remove("quit")
392 /// .build()
393 /// );
394 /// ```
395 pub fn set_commands(
396 &mut self,
397 commands: Vec<Box<dyn crate::tui::commands::SlashCommand>>,
398 ) -> &mut Self {
399 self.commands = Some(commands);
400 self
401 }
402
403 /// Set extension data available to custom commands.
404 ///
405 /// Commands can access this via `ctx.extension::<T>()`.
406 ///
407 /// # Example
408 ///
409 /// ```ignore
410 /// struct MyContext { api_key: String }
411 ///
412 /// agent.set_command_extension(MyContext {
413 /// api_key: "secret".to_string()
414 /// });
415 /// ```
416 pub fn set_command_extension<T: std::any::Any + Send + 'static>(
417 &mut self,
418 ext: T,
419 ) -> &mut Self {
420 self.command_extension = Some(Box::new(ext));
421 self
422 }
423
424 /// Register tools with the agent.
425 ///
426 /// The callback receives references to the tool registry and interaction registries,
427 /// and should return the tool definitions to register.
428 ///
429 /// # Example
430 ///
431 /// ```ignore
432 /// core.register_tools(|registry, user_reg, perm_reg| {
433 /// tools::register_all_tools(registry, user_reg, perm_reg)
434 /// })?;
435 /// ```
436 pub fn register_tools<F>(&mut self, f: F) -> Result<(), AgentError>
437 where
438 F: FnOnce(
439 &Arc<ToolRegistry>,
440 &Arc<UserInteractionRegistry>,
441 &Arc<PermissionRegistry>,
442 ) -> Result<Vec<LLMTool>, String>,
443 {
444 let tool_defs = f(
445 self.controller.tool_registry(),
446 &self.user_interaction_registry,
447 &self.permission_registry,
448 )
449 .map_err(AgentError::ToolRegistration)?;
450 self.tool_definitions = tool_defs;
451 Ok(())
452 }
453
454 /// Register tools with the agent using an async function.
455 ///
456 /// Similar to `register_tools`, but accepts an async closure. The closure
457 /// is executed using the agent's tokio runtime via `block_on`.
458 ///
459 /// # Example
460 ///
461 /// ```ignore
462 /// core.register_tools_async(|registry, user_reg, perm_reg| async move {
463 /// tools::register_all_tools(®istry, user_reg, perm_reg).await
464 /// })?;
465 /// ```
466 pub fn register_tools_async<F, Fut>(&mut self, f: F) -> Result<(), AgentError>
467 where
468 F: FnOnce(Arc<ToolRegistry>, Arc<UserInteractionRegistry>, Arc<PermissionRegistry>) -> Fut,
469 Fut: std::future::Future<Output = Result<Vec<LLMTool>, String>>,
470 {
471 let tool_defs = self.runtime.block_on(f(
472 self.controller.tool_registry().clone(),
473 self.user_interaction_registry.clone(),
474 self.permission_registry.clone(),
475 ))
476 .map_err(AgentError::ToolRegistration)?;
477 self.tool_definitions = tool_defs;
478 Ok(())
479 }
480
481 /// Register a widget with the agent.
482 ///
483 /// Widgets are registered before calling `run()` and will be available
484 /// in the TUI application.
485 ///
486 /// # Example
487 ///
488 /// ```ignore
489 /// let mut agent = AgentCore::new(&MyConfig)?;
490 /// agent.register_widget(PermissionPanel::new());
491 /// agent.register_widget(QuestionPanel::new());
492 /// agent.run()
493 /// ```
494 pub fn register_widget<W: Widget>(&mut self, widget: W) -> &mut Self {
495 self.widgets_to_register.push(Box::new(widget));
496 self
497 }
498
499 /// Set a custom status bar widget to replace the default.
500 ///
501 /// This will unregister the default status bar and register the custom one.
502 ///
503 /// # Example
504 ///
505 /// ```ignore
506 /// use agent_core::tui::{StatusBar, StatusBarConfig};
507 ///
508 /// let mut agent = AgentCore::new(&MyConfig)?;
509 /// let custom_status_bar = StatusBar::new()
510 /// .with_renderer(|data, theme| {
511 /// vec![Line::from(format!(" {} | {}", data.model_name, data.session_id))]
512 /// });
513 /// agent.set_status_bar(custom_status_bar);
514 /// agent.run()
515 /// ```
516 pub fn set_status_bar<W: Widget>(&mut self, status_bar: W) -> &mut Self {
517 self.custom_status_bar = Some(Box::new(status_bar));
518 self
519 }
520
521 /// Hide the default status bar.
522 ///
523 /// This will unregister the default status bar widget. Useful for minimal layouts
524 /// or when you want to implement your own status display.
525 ///
526 /// # Example
527 ///
528 /// ```ignore
529 /// let mut agent = AgentCore::new(&MyConfig)?;
530 /// agent.hide_status_bar();
531 /// agent.run()
532 /// ```
533 pub fn hide_status_bar(&mut self) -> &mut Self {
534 self.hide_status_bar = true;
535 self
536 }
537
538 /// Start the controller and input router as background tasks.
539 ///
540 /// This must be called before sending messages or creating sessions.
541 /// After calling this, the controller is running and ready to accept input.
542 pub fn start_background_tasks(&mut self) {
543 tracing::info!("{} starting background tasks", self.name);
544
545 // Start the controller event loop in a background task
546 let controller = self.controller.clone();
547 self.runtime.spawn(async move {
548 controller.start().await;
549 });
550 tracing::info!("Controller started");
551
552 // Start the input router in a background task
553 if let Some(to_controller_rx) = self.to_controller_rx.take() {
554 let router = InputRouter::new(
555 self.controller.clone(),
556 to_controller_rx,
557 self.cancel_token.clone(),
558 );
559 self.runtime.spawn(async move {
560 router.run().await;
561 });
562 tracing::info!("InputRouter started");
563 }
564 }
565
566 /// Internal helper to create a session and configure tools.
567 async fn create_session_internal(
568 controller: &Arc<LLMController>,
569 config: LLMSessionConfig,
570 tools: &[LLMTool],
571 ) -> Result<i64, crate::client::error::LlmError> {
572 let id = controller.create_session(config).await?;
573
574 // Set tools on the session after creation
575 if !tools.is_empty() {
576 if let Some(session) = controller.get_session(id).await {
577 session.set_tools(tools.to_vec()).await;
578 }
579 }
580
581 Ok(id)
582 }
583
584 /// Create an initial session using the default LLM provider.
585 ///
586 /// Returns the session ID, model name, and context limit.
587 pub fn create_initial_session(&mut self) -> Result<(i64, String, i32), AgentError> {
588 let registry = self.llm_registry.as_ref().ok_or_else(|| {
589 AgentError::NoConfiguration("No LLM registry available".to_string())
590 })?;
591
592 let config = registry.get_default().ok_or_else(|| {
593 AgentError::NoConfiguration("No default LLM provider configured".to_string())
594 })?;
595
596 let model = config.model.clone();
597 let context_limit = config.context_limit;
598
599 let controller = self.controller.clone();
600 let tool_definitions = self.tool_definitions.clone();
601
602 let session_id = self.runtime.block_on(Self::create_session_internal(
603 &controller,
604 config.clone(),
605 &tool_definitions,
606 ))?;
607
608 tracing::info!(
609 session_id = session_id,
610 model = %model,
611 "Created initial session"
612 );
613
614 Ok((session_id, model, context_limit))
615 }
616
617 /// Create a session with the given configuration.
618 ///
619 /// Returns the session ID or an error.
620 pub fn create_session(&self, config: LLMSessionConfig) -> Result<i64, AgentError> {
621 let controller = self.controller.clone();
622 let tool_definitions = self.tool_definitions.clone();
623
624 self.runtime
625 .block_on(Self::create_session_internal(
626 &controller,
627 config,
628 &tool_definitions,
629 ))
630 .map_err(AgentError::from)
631 }
632
633 /// Signal shutdown to all background tasks and the controller.
634 pub fn shutdown(&self) {
635 tracing::info!("{} shutting down", self.name);
636 self.cancel_token.cancel();
637
638 let controller = self.controller.clone();
639 self.runtime.block_on(async move {
640 controller.shutdown().await;
641 });
642
643 tracing::info!("{} shutdown complete", self.name);
644 }
645
646 /// Run the agent with the default TUI.
647 ///
648 /// This is the main entry point for running an agent. It:
649 /// 1. Starts background tasks (controller, input router)
650 /// 2. Creates an App with the configured settings
651 /// 3. Wires up all channels and registries
652 /// 4. Creates an initial session if LLM providers are configured
653 /// 5. Runs the TUI event loop
654 /// 6. Shuts down cleanly when the user quits
655 ///
656 /// # Example
657 ///
658 /// ```ignore
659 /// fn main() -> io::Result<()> {
660 /// let mut agent = AgentCore::new(&MyConfig)?;
661 /// agent.run()
662 /// }
663 /// ```
664 pub fn run(&mut self) -> io::Result<()> {
665 tracing::info!("{} starting", self.name);
666
667 // Start background tasks (controller, input router)
668 self.start_background_tasks();
669
670 // Create App with our configuration
671 let app_config = AppConfig {
672 agent_name: self.name.clone(),
673 version: self.version.clone(),
674 commands: self.commands.take(),
675 command_extension: self.command_extension.take(),
676 ..Default::default()
677 };
678 let mut app = App::with_config(app_config);
679
680 // Set conversation factory if provided
681 if let Some(factory) = self.conversation_factory.take() {
682 app.set_conversation_factory(move || factory());
683 }
684
685 // Handle status bar customization
686 if self.hide_status_bar {
687 // Remove the default status bar
688 app.widgets.remove(crate::tui::widgets::widget_ids::STATUS_BAR);
689 } else if let Some(custom_status_bar) = self.custom_status_bar.take() {
690 // Replace default status bar with custom one
691 app.widgets.insert(crate::tui::widgets::widget_ids::STATUS_BAR, custom_status_bar);
692 }
693
694 // Register widgets with the App
695 for widget in self.widgets_to_register.drain(..) {
696 // We need to re-box as the App's register_widget expects impl Widget
697 let id = widget.id();
698 app.widgets.insert(id, widget);
699 }
700 app.rebuild_priority_order();
701
702 // Wire up channels, controller, and registries to the App
703 app.set_to_controller(self.to_controller_tx.clone());
704 if let Some(rx) = self.from_controller_rx.take() {
705 app.set_from_controller(rx);
706 }
707 app.set_controller(self.controller.clone());
708 app.set_runtime_handle(self.runtime.handle().clone());
709 app.set_user_interaction_registry(self.user_interaction_registry.clone());
710 app.set_permission_registry(self.permission_registry.clone());
711
712 // Set layout template if specified
713 if let Some(layout) = self.layout_template.take() {
714 app.set_layout(layout);
715 }
716
717 // Set key handler if specified
718 if let Some(handler) = self.key_handler.take() {
719 app.set_key_handler_boxed(handler);
720 }
721
722 // Set exit handler if specified
723 if let Some(handler) = self.exit_handler.take() {
724 app.set_exit_handler_boxed(handler);
725 }
726
727 // Auto-create session if we have a configured LLM provider
728 match self.create_initial_session() {
729 Ok((session_id, model, context_limit)) => {
730 let session_info = SessionInfo::new(session_id, model.clone(), context_limit);
731 app.add_session(session_info);
732 app.set_session_id(session_id);
733 app.set_model_name(&model);
734 app.set_context_limit(context_limit);
735 tracing::info!(
736 session_id = session_id,
737 model = %model,
738 "Auto-created session on startup"
739 );
740 }
741 Err(e) => {
742 tracing::warn!(error = %e, "No initial session created");
743 }
744 }
745
746 // Pass LLM registry to app for creating new sessions
747 if let Some(registry) = self.llm_registry.take() {
748 app.set_llm_registry(registry);
749 }
750
751 // Run the TUI (blocking)
752 let result = app.run();
753
754 // Shutdown the agent
755 self.shutdown();
756
757 tracing::info!("{} stopped", self.name);
758 result
759 }
760
761 // ---- Accessors ----
762
763 /// Returns a sender for sending messages to the controller.
764 pub fn to_controller_tx(&self) -> ToControllerTx {
765 self.to_controller_tx.clone()
766 }
767
768 /// Takes the receiver for messages from the controller (can only be called once).
769 pub fn take_from_controller_rx(&mut self) -> Option<FromControllerRx> {
770 self.from_controller_rx.take()
771 }
772
773 /// Returns a reference to the controller.
774 pub fn controller(&self) -> &Arc<LLMController> {
775 &self.controller
776 }
777
778 /// Returns a reference to the runtime.
779 pub fn runtime(&self) -> &Runtime {
780 &self.runtime
781 }
782
783 /// Returns a handle to the runtime.
784 pub fn runtime_handle(&self) -> tokio::runtime::Handle {
785 self.runtime.handle().clone()
786 }
787
788 /// Returns a reference to the user interaction registry.
789 pub fn user_interaction_registry(&self) -> &Arc<UserInteractionRegistry> {
790 &self.user_interaction_registry
791 }
792
793 /// Returns a reference to the permission registry.
794 pub fn permission_registry(&self) -> &Arc<PermissionRegistry> {
795 &self.permission_registry
796 }
797
798 /// Returns a reference to the LLM registry.
799 pub fn llm_registry(&self) -> Option<&LLMRegistry> {
800 self.llm_registry.as_ref()
801 }
802
803 /// Takes the LLM registry (can only be called once).
804 pub fn take_llm_registry(&mut self) -> Option<LLMRegistry> {
805 self.llm_registry.take()
806 }
807
808 /// Returns the cancellation token.
809 pub fn cancel_token(&self) -> CancellationToken {
810 self.cancel_token.clone()
811 }
812
813 /// Returns the agent name.
814 pub fn name(&self) -> &str {
815 &self.name
816 }
817
818 /// Returns a clone of the UI message sender.
819 ///
820 /// This can be used to send messages to the App's UI event loop.
821 pub fn from_controller_tx(&self) -> FromControllerTx {
822 self.from_controller_tx.clone()
823 }
824}
825
826/// Converts a ControllerEvent to a UiMessage for the TUI.
827///
828/// This function maps the internal controller events to UI-friendly messages
829/// that can be displayed in a terminal interface.
830pub fn convert_controller_event_to_ui_message(event: ControllerEvent) -> UiMessage {
831 match event {
832 ControllerEvent::StreamStart { session_id, .. } => {
833 // Silent - don't display stream start messages
834 UiMessage::System {
835 session_id,
836 message: String::new(),
837 }
838 }
839 ControllerEvent::TextChunk {
840 session_id,
841 text,
842 turn_id,
843 } => UiMessage::TextChunk {
844 session_id,
845 turn_id,
846 text,
847 input_tokens: 0,
848 output_tokens: 0,
849 },
850 ControllerEvent::ToolUseStart {
851 session_id,
852 tool_name,
853 turn_id,
854 ..
855 } => UiMessage::Display {
856 session_id,
857 turn_id,
858 message: format!("Executing tool: {}", tool_name),
859 },
860 ControllerEvent::ToolUse {
861 session_id,
862 tool,
863 display_name,
864 display_title,
865 turn_id,
866 } => UiMessage::ToolExecuting {
867 session_id,
868 turn_id,
869 tool_use_id: tool.id.clone(),
870 display_name: display_name.unwrap_or_else(|| tool.name.clone()),
871 display_title: display_title.unwrap_or_default(),
872 },
873 ControllerEvent::Complete {
874 session_id,
875 turn_id,
876 stop_reason,
877 } => UiMessage::Complete {
878 session_id,
879 turn_id,
880 input_tokens: 0,
881 output_tokens: 0,
882 stop_reason,
883 },
884 ControllerEvent::Error {
885 session_id,
886 error,
887 turn_id,
888 } => UiMessage::Error {
889 session_id,
890 turn_id,
891 error,
892 },
893 ControllerEvent::TokenUpdate {
894 session_id,
895 input_tokens,
896 output_tokens,
897 context_limit,
898 } => UiMessage::TokenUpdate {
899 session_id,
900 turn_id: None,
901 input_tokens,
902 output_tokens,
903 context_limit,
904 },
905 ControllerEvent::ToolResult {
906 session_id,
907 tool_use_id,
908 status,
909 error,
910 turn_id,
911 ..
912 } => UiMessage::ToolCompleted {
913 session_id,
914 turn_id,
915 tool_use_id,
916 status,
917 error,
918 },
919 ControllerEvent::CommandComplete {
920 session_id,
921 command,
922 success,
923 message,
924 } => UiMessage::CommandComplete {
925 session_id,
926 command,
927 success,
928 message,
929 },
930 ControllerEvent::UserInteractionRequired {
931 session_id,
932 tool_use_id,
933 request,
934 turn_id,
935 } => UiMessage::UserInteractionRequired {
936 session_id,
937 tool_use_id,
938 request,
939 turn_id,
940 },
941 ControllerEvent::PermissionRequired {
942 session_id,
943 tool_use_id,
944 request,
945 turn_id,
946 } => UiMessage::PermissionRequired {
947 session_id,
948 tool_use_id,
949 request,
950 turn_id,
951 },
952 }
953}
954
955#[cfg(test)]
956mod tests {
957 use super::*;
958 use crate::controller::TurnId;
959
960 #[test]
961 fn test_convert_text_chunk_event() {
962 let event = ControllerEvent::TextChunk {
963 session_id: 1,
964 text: "Hello".to_string(),
965 turn_id: Some(TurnId::new_user_turn(1)),
966 };
967
968 let msg = convert_controller_event_to_ui_message(event);
969
970 match msg {
971 UiMessage::TextChunk {
972 session_id, text, ..
973 } => {
974 assert_eq!(session_id, 1);
975 assert_eq!(text, "Hello");
976 }
977 _ => panic!("Expected TextChunk message"),
978 }
979 }
980
981 #[test]
982 fn test_convert_error_event() {
983 let event = ControllerEvent::Error {
984 session_id: 1,
985 error: "Test error".to_string(),
986 turn_id: None,
987 };
988
989 let msg = convert_controller_event_to_ui_message(event);
990
991 match msg {
992 UiMessage::Error {
993 session_id, error, ..
994 } => {
995 assert_eq!(session_id, 1);
996 assert_eq!(error, "Test error");
997 }
998 _ => panic!("Expected Error message"),
999 }
1000 }
1001}