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(&registry, 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}