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