Skip to main content

agent_core_runtime/agent/
core.rs

1// AgentCore - Core runtime infrastructure for LLM-powered agents
2//
3// This module provides the runtime engine without any TUI dependencies.
4// For TUI functionality, use agent-core-tui which extends this with run().
5
6use std::io;
7use std::sync::Arc;
8
9use tokio::runtime::Runtime;
10use tokio::sync::mpsc;
11use tokio_util::sync::CancellationToken;
12
13use crate::controller::{
14    ControllerEvent, ControllerInputPayload, Executable, LLMController, LLMSessionConfig,
15    LLMTool, ListSkillsTool, PermissionRegistry, ToolRegistry, UserInteractionRegistry,
16};
17use crate::skills::{SkillDiscovery, SkillDiscoveryError, SkillRegistry, SkillReloadResult};
18
19use super::config::{load_config, AgentConfig, LLMRegistry};
20use super::error::AgentError;
21use super::logger::Logger;
22use super::messages::channels::DEFAULT_CHANNEL_SIZE;
23use super::messages::UiMessage;
24use super::router::InputRouter;
25
26/// Sender for messages from frontend to controller
27pub type ToControllerTx = mpsc::Sender<ControllerInputPayload>;
28/// Receiver for messages from frontend to controller
29pub type ToControllerRx = mpsc::Receiver<ControllerInputPayload>;
30/// Sender for messages from controller to frontend
31pub type FromControllerTx = mpsc::Sender<UiMessage>;
32/// Receiver for messages from controller to frontend
33pub type FromControllerRx = mpsc::Receiver<UiMessage>;
34
35/// AgentCore - Core runtime infrastructure for LLM-powered agents.
36///
37/// AgentCore provides all the infrastructure needed for an LLM-powered agent:
38/// - Logging with tracing
39/// - LLM configuration loading
40/// - Tokio async runtime
41/// - LLMController for session management
42/// - Communication channels
43/// - User interaction and permission registries
44///
45/// This is the runtime-only version. For TUI support, use the `agent-core` crate
46/// with the `tui` feature enabled, which provides the `run()` method.
47///
48/// # Basic Usage (Headless)
49///
50/// ```ignore
51/// use agent_core_runtime::agent::{AgentConfig, AgentCore};
52///
53/// struct MyConfig;
54/// impl AgentConfig for MyConfig {
55///     fn config_path(&self) -> &str { ".myagent/config.yaml" }
56///     fn default_system_prompt(&self) -> &str { "You are helpful." }
57///     fn log_prefix(&self) -> &str { "myagent" }
58///     fn name(&self) -> &str { "MyAgent" }
59/// }
60///
61/// fn main() -> std::io::Result<()> {
62///     let mut core = AgentCore::new(&MyConfig)?;
63///     core.start_background_tasks();
64///
65///     // Get channels for custom frontend integration
66///     let tx = core.to_controller_tx();
67///     let rx = core.take_from_controller_rx();
68///
69///     // Create a session and interact programmatically
70///     let (session_id, model, _) = core.create_initial_session()?;
71///     // ... send messages and receive responses via channels
72///
73///     core.shutdown();
74///     Ok(())
75/// }
76/// ```
77pub struct AgentCore {
78    /// Logger instance - never directly accessed but must be kept alive for RAII.
79    /// Dropping this field would stop logging, so it's held for the lifetime of AgentCore.
80    #[allow(dead_code)]
81    logger: Logger,
82
83    /// Agent name for display
84    name: String,
85
86    /// Agent version for display
87    version: String,
88
89    /// Tokio runtime for async operations
90    runtime: Runtime,
91
92    /// The LLM controller
93    controller: Arc<LLMController>,
94
95    /// LLM provider registry (loaded from config)
96    llm_registry: Option<LLMRegistry>,
97
98    /// Sender for messages from frontend to controller
99    to_controller_tx: ToControllerTx,
100
101    /// Receiver for messages from frontend to controller (consumed by InputRouter)
102    to_controller_rx: Option<ToControllerRx>,
103
104    /// Sender for messages from controller to frontend (held by event handler)
105    from_controller_tx: FromControllerTx,
106
107    /// Receiver for messages from controller to frontend
108    from_controller_rx: Option<FromControllerRx>,
109
110    /// Cancellation token for graceful shutdown
111    cancel_token: CancellationToken,
112
113    /// User interaction registry for AskUserQuestions tool
114    user_interaction_registry: Arc<UserInteractionRegistry>,
115
116    /// Permission registry for AskForPermissions tool
117    permission_registry: Arc<PermissionRegistry>,
118
119    /// Tool definitions to register on sessions
120    tool_definitions: Vec<LLMTool>,
121
122    /// Error message shown when user submits but no session exists
123    error_no_session: Option<String>,
124
125    /// Skill registry for Agent Skills support
126    skill_registry: Arc<SkillRegistry>,
127
128    /// Skill discovery paths
129    skill_discovery: SkillDiscovery,
130}
131
132impl AgentCore {
133    /// Create a new AgentCore with the given configuration.
134    ///
135    /// This initializes:
136    /// - Logging infrastructure
137    /// - LLM configuration from config file or environment
138    /// - Tokio runtime
139    /// - Communication channels
140    /// - LLMController
141    /// - User interaction and permission registries
142    pub fn new<C: AgentConfig>(config: &C) -> io::Result<Self> {
143        let logger = Logger::new(config.log_prefix())?;
144        tracing::info!("{} agent initialized", config.name());
145
146        // Load LLM configuration
147        let llm_registry = load_config(config);
148        if llm_registry.is_empty() {
149            tracing::warn!(
150                "No LLM providers configured. Set ANTHROPIC_API_KEY or create ~/{}",
151                config.config_path()
152            );
153        } else {
154            tracing::info!(
155                "Loaded {} LLM provider(s): {:?}",
156                llm_registry.providers().len(),
157                llm_registry.providers()
158            );
159        }
160
161        // Create tokio runtime for async operations
162        let runtime = Runtime::new().map_err(|e| {
163            io::Error::new(
164                io::ErrorKind::Other,
165                format!("Failed to create runtime: {}", e),
166            )
167        })?;
168
169        // Get channel buffer size from config (or use default)
170        let channel_size = config.channel_buffer_size().unwrap_or(DEFAULT_CHANNEL_SIZE);
171        tracing::debug!("Using channel buffer size: {}", channel_size);
172
173        // Create communication channels
174        let (to_controller_tx, to_controller_rx) =
175            mpsc::channel::<ControllerInputPayload>(channel_size);
176        let (from_controller_tx, from_controller_rx) =
177            mpsc::channel::<UiMessage>(channel_size);
178
179        // Create channel for user interaction events
180        let (interaction_event_tx, mut interaction_event_rx) =
181            mpsc::channel::<ControllerEvent>(channel_size);
182
183        // Create the user interaction registry
184        let user_interaction_registry =
185            Arc::new(UserInteractionRegistry::new(interaction_event_tx));
186
187        // Spawn a task to forward user interaction events to the UI channel
188        // Uses blocking send for backpressure
189        let ui_tx_for_interactions = from_controller_tx.clone();
190        runtime.spawn(async move {
191            while let Some(event) = interaction_event_rx.recv().await {
192                let msg = convert_controller_event_to_ui_message(event);
193                if let Err(e) = ui_tx_for_interactions.send(msg).await {
194                    tracing::warn!("Failed to send user interaction event to UI: {}", e);
195                }
196            }
197        });
198
199        // Create channel for permission events
200        let (permission_event_tx, mut permission_event_rx) =
201            mpsc::channel::<ControllerEvent>(channel_size);
202
203        // Create the permission registry
204        let permission_registry = Arc::new(PermissionRegistry::new(permission_event_tx));
205
206        // Spawn a task to forward permission events to the UI channel
207        // Uses blocking send for backpressure
208        let ui_tx_for_permissions = from_controller_tx.clone();
209        runtime.spawn(async move {
210            while let Some(event) = permission_event_rx.recv().await {
211                let msg = convert_controller_event_to_ui_message(event);
212                if let Err(e) = ui_tx_for_permissions.send(msg).await {
213                    tracing::warn!("Failed to send permission event to UI: {}", e);
214                }
215            }
216        });
217
218        // Create the controller with UI channel for direct event forwarding
219        // The controller will use backpressure: when UI channel is full, it stops
220        // reading from LLM, which backs up the from_llm channel, which blocks the
221        // session, which slows down network consumption.
222        let controller = Arc::new(LLMController::new(
223            permission_registry.clone(),
224            Some(from_controller_tx.clone()),
225            Some(channel_size),
226        ));
227        let cancel_token = CancellationToken::new();
228
229        Ok(Self {
230            logger,
231            name: config.name().to_string(),
232            version: "0.1.0".to_string(),
233            runtime,
234            controller,
235            llm_registry: Some(llm_registry),
236            to_controller_tx,
237            to_controller_rx: Some(to_controller_rx),
238            from_controller_tx,
239            from_controller_rx: Some(from_controller_rx),
240            cancel_token,
241            user_interaction_registry,
242            permission_registry,
243            tool_definitions: Vec::new(),
244            error_no_session: None,
245            skill_registry: Arc::new(SkillRegistry::new()),
246            skill_discovery: SkillDiscovery::new(),
247        })
248    }
249
250    /// Create a new AgentCore with simple configuration parameters.
251    ///
252    /// This is a convenience constructor for quick agent setup without
253    /// defining a custom config struct.
254    ///
255    /// # Arguments
256    /// * `name` - Agent name for display (e.g., "my-agent")
257    /// * `config_path` - Path to config file (e.g., "~/.config/my-agent/config.yaml")
258    /// * `system_prompt` - Default system prompt for the agent
259    ///
260    /// # Example
261    ///
262    /// ```ignore
263    /// use agent_core::agent::AgentCore;
264    /// use agent_core::tui::AgentCoreExt;
265    ///
266    /// AgentCore::with_config("my-agent", "~/.config/my-agent/config.yaml", "You are helpful.")?
267    ///     .into_tui()
268    ///     .run()
269    /// ```
270    pub fn with_config(
271        name: impl Into<String>,
272        config_path: impl Into<String>,
273        system_prompt: impl Into<String>,
274    ) -> io::Result<Self> {
275        let config = super::config::SimpleConfig::new(name, config_path, system_prompt);
276        Self::new(&config)
277    }
278
279    /// Set the error message shown when user submits but no session exists.
280    ///
281    /// This overrides the default message "No active session. Use /new-session to create one."
282    ///
283    /// # Example
284    ///
285    /// ```ignore
286    /// agent.set_error_no_session("No configuration found in ~/.myagent/config.yaml");
287    /// ```
288    pub fn set_error_no_session(&mut self, message: impl Into<String>) -> &mut Self {
289        self.error_no_session = Some(message.into());
290        self
291    }
292
293    /// Get the error message for no session, if set.
294    pub fn error_no_session(&self) -> Option<&str> {
295        self.error_no_session.as_deref()
296    }
297
298    /// Set the agent version for display.
299    pub fn set_version(&mut self, version: impl Into<String>) {
300        self.version = version.into();
301    }
302
303    /// Get the agent version.
304    pub fn version(&self) -> &str {
305        &self.version
306    }
307
308    /// Load environment context into the system prompt.
309    ///
310    /// This adds information about the current execution environment to
311    /// all LLM session prompts:
312    /// - Current working directory
313    /// - Platform (darwin, linux, windows)
314    /// - OS version
315    /// - Today's date
316    ///
317    /// The context is wrapped in `<env>` tags and appended to the system prompt.
318    ///
319    /// # Example
320    ///
321    /// ```ignore
322    /// let mut core = AgentCore::new(&config)?;
323    /// core.load_environment_context();
324    /// ```
325    pub fn load_environment_context(&mut self) -> &mut Self {
326        if let Some(registry) = self.llm_registry.take() {
327            self.llm_registry = Some(registry.with_environment_context());
328            tracing::info!("Environment context loaded into system prompt");
329        }
330        self
331    }
332
333    /// Register tools with the agent.
334    ///
335    /// The callback receives references to the tool registry and interaction registries,
336    /// and should return the tool definitions to register.
337    ///
338    /// # Example
339    ///
340    /// ```ignore
341    /// core.register_tools(|registry, user_reg, perm_reg| {
342    ///     tools::register_all_tools(registry, user_reg, perm_reg)
343    /// })?;
344    /// ```
345    pub fn register_tools<F>(&mut self, f: F) -> Result<(), AgentError>
346    where
347        F: FnOnce(
348            &Arc<ToolRegistry>,
349            &Arc<UserInteractionRegistry>,
350            &Arc<PermissionRegistry>,
351        ) -> Result<Vec<LLMTool>, String>,
352    {
353        let tool_defs = f(
354            self.controller.tool_registry(),
355            &self.user_interaction_registry,
356            &self.permission_registry,
357        )
358        .map_err(AgentError::ToolRegistration)?;
359        self.tool_definitions = tool_defs;
360        Ok(())
361    }
362
363    /// Register tools with the agent using an async function.
364    ///
365    /// Similar to `register_tools`, but accepts an async closure. The closure
366    /// is executed using the agent's tokio runtime via `block_on`.
367    ///
368    /// # Example
369    ///
370    /// ```ignore
371    /// core.register_tools_async(|registry, user_reg, perm_reg| async move {
372    ///     tools::register_all_tools(&registry, user_reg, perm_reg).await
373    /// })?;
374    /// ```
375    pub fn register_tools_async<F, Fut>(&mut self, f: F) -> Result<(), AgentError>
376    where
377        F: FnOnce(Arc<ToolRegistry>, Arc<UserInteractionRegistry>, Arc<PermissionRegistry>) -> Fut,
378        Fut: std::future::Future<Output = Result<Vec<LLMTool>, String>>,
379    {
380        let tool_defs = self.runtime.block_on(f(
381            self.controller.tool_registry().clone(),
382            self.user_interaction_registry.clone(),
383            self.permission_registry.clone(),
384        ))
385        .map_err(AgentError::ToolRegistration)?;
386        self.tool_definitions = tool_defs;
387        Ok(())
388    }
389
390    /// Start the controller and input router as background tasks.
391    ///
392    /// This must be called before sending messages or creating sessions.
393    /// After calling this, the controller is running and ready to accept input.
394    pub fn start_background_tasks(&mut self) {
395        tracing::info!("{} starting background tasks", self.name);
396
397        // Start the controller event loop in a background task
398        let controller = self.controller.clone();
399        self.runtime.spawn(async move {
400            controller.start().await;
401        });
402        tracing::info!("Controller started");
403
404        // Start the input router in a background task
405        if let Some(to_controller_rx) = self.to_controller_rx.take() {
406            let router = InputRouter::new(
407                self.controller.clone(),
408                to_controller_rx,
409                self.cancel_token.clone(),
410            );
411            self.runtime.spawn(async move {
412                router.run().await;
413            });
414            tracing::info!("InputRouter started");
415        }
416    }
417
418    /// Internal helper to create a session and configure tools.
419    async fn create_session_internal(
420        controller: &Arc<LLMController>,
421        mut config: LLMSessionConfig,
422        tools: &[LLMTool],
423        skill_registry: &Arc<SkillRegistry>,
424    ) -> Result<i64, crate::client::error::LlmError> {
425        // Inject skills XML into system prompt
426        let skills_xml = skill_registry.to_prompt_xml();
427        if !skills_xml.is_empty() {
428            config.system_prompt = Some(match config.system_prompt {
429                Some(prompt) => format!("{}\n\n{}", prompt, skills_xml),
430                None => skills_xml,
431            });
432        }
433
434        let id = controller.create_session(config).await?;
435
436        // Set tools on the session after creation
437        if !tools.is_empty() {
438            if let Some(session) = controller.get_session(id).await {
439                session.set_tools(tools.to_vec()).await;
440            }
441        }
442
443        Ok(id)
444    }
445
446    /// Create an initial session using the default LLM provider.
447    ///
448    /// Returns the session ID, model name, and context limit.
449    pub fn create_initial_session(&mut self) -> Result<(i64, String, i32), AgentError> {
450        let registry = self.llm_registry.as_ref().ok_or_else(|| {
451            AgentError::NoConfiguration("No LLM registry available".to_string())
452        })?;
453
454        let config = registry.get_default().ok_or_else(|| {
455            AgentError::NoConfiguration("No default LLM provider configured".to_string())
456        })?;
457
458        let model = config.model.clone();
459        let context_limit = config.context_limit;
460
461        let controller = self.controller.clone();
462        let tool_definitions = self.tool_definitions.clone();
463        let skill_registry = self.skill_registry.clone();
464
465        let session_id = self.runtime.block_on(Self::create_session_internal(
466            &controller,
467            config.clone(),
468            &tool_definitions,
469            &skill_registry,
470        ))?;
471
472        tracing::info!(
473            session_id = session_id,
474            model = %model,
475            "Created initial session"
476        );
477
478        Ok((session_id, model, context_limit))
479    }
480
481    /// Create a session with the given configuration.
482    ///
483    /// Returns the session ID or an error.
484    pub fn create_session(&self, config: LLMSessionConfig) -> Result<i64, AgentError> {
485        let controller = self.controller.clone();
486        let tool_definitions = self.tool_definitions.clone();
487        let skill_registry = self.skill_registry.clone();
488
489        self.runtime
490            .block_on(Self::create_session_internal(
491                &controller,
492                config,
493                &tool_definitions,
494                &skill_registry,
495            ))
496            .map_err(AgentError::from)
497    }
498
499    /// Signal shutdown to all background tasks and the controller.
500    pub fn shutdown(&self) {
501        tracing::info!("{} shutting down", self.name);
502        self.cancel_token.cancel();
503
504        let controller = self.controller.clone();
505        self.runtime.block_on(async move {
506            controller.shutdown().await;
507        });
508
509        tracing::info!("{} shutdown complete", self.name);
510    }
511
512    // ---- Custom Frontend Support ----
513
514    /// Run the agent with a custom frontend.
515    ///
516    /// This is the primary entry point for custom frontends. It:
517    /// 1. Starts background tasks (controller, input router)
518    /// 2. Wires the event sink to receive engine events
519    /// 3. Wires the input source to provide user input
520    /// 4. Applies the permission policy
521    /// 5. Runs until the input source closes
522    ///
523    /// # Arguments
524    ///
525    /// * `event_sink` - Receives events from the engine
526    /// * `input_source` - Provides input to the engine
527    /// * `permission_policy` - Handles permission requests
528    ///
529    /// # Example: Headless with Auto-Approve
530    ///
531    /// ```ignore
532    /// use agent_core_runtime::agent::{
533    ///     AgentCore, AutoApprovePolicy, StdoutEventSink, ChannelInputSource
534    /// };
535    ///
536    /// let mut agent = AgentCore::with_config(
537    ///     "my-agent",
538    ///     "~/.config/my-agent/config.yaml",
539    ///     "You are helpful."
540    /// )?;
541    ///
542    /// // Create input channel
543    /// let (input_tx, input_source) = ChannelInputSource::channel(100);
544    ///
545    /// // Run with custom frontend (blocks until input_tx is dropped)
546    /// agent.run_with_frontend(
547    ///     StdoutEventSink::new(),
548    ///     input_source,
549    ///     AutoApprovePolicy::new(),
550    /// )?;
551    /// ```
552    pub fn run_with_frontend<E, I, P>(
553        &mut self,
554        event_sink: E,
555        mut input_source: I,
556        permission_policy: P,
557    ) -> io::Result<()>
558    where
559        E: super::interface::EventSink,
560        I: super::interface::InputSource,
561        P: super::interface::PermissionPolicy,
562    {
563        use std::sync::Arc;
564        use super::interface::PolicyDecision;
565        use crate::permissions::{BatchPermissionResponse, PermissionPanelResponse};
566
567        tracing::info!("{} starting with custom frontend", self.name);
568
569        // Wrap sink in Arc for sharing with event forwarder
570        let sink = Arc::new(event_sink);
571        let policy = Arc::new(permission_policy);
572
573        // Start background tasks (controller, but not the default input router)
574        // We'll handle input ourselves
575        let controller = self.controller.clone();
576        self.runtime.spawn(async move {
577            controller.start().await;
578        });
579        tracing::info!("Controller started");
580
581        // Set up event forwarding from controller to custom sink
582        // Take the from_controller_rx if available
583        if let Some(mut from_controller_rx) = self.from_controller_rx.take() {
584            let sink_clone = sink.clone();
585            let policy_clone = policy.clone();
586            let permission_registry = self.permission_registry.clone();
587            let user_interaction_registry = self.user_interaction_registry.clone();
588
589            self.runtime.spawn(async move {
590                while let Some(event) = from_controller_rx.recv().await {
591                    // Check if this is a permission request that should be handled by policy
592                    match &event {
593                        UiMessage::PermissionRequired { tool_use_id, request, .. } => {
594                            match policy_clone.decide(request) {
595                                PolicyDecision::AskUser => {
596                                    // Fall through to forward to sink
597                                }
598                                decision => {
599                                    let response = match decision {
600                                        PolicyDecision::Allow => PermissionPanelResponse {
601                                            granted: true,
602                                            grant: None,
603                                            message: None,
604                                        },
605                                        PolicyDecision::AllowWithGrant(grant) => PermissionPanelResponse {
606                                            granted: true,
607                                            grant: Some(grant),
608                                            message: None,
609                                        },
610                                        PolicyDecision::Deny { reason } => PermissionPanelResponse {
611                                            granted: false,
612                                            grant: None,
613                                            message: reason,
614                                        },
615                                        PolicyDecision::AskUser => unreachable!(),
616                                    };
617                                    if let Err(e) = permission_registry
618                                        .respond_to_request(tool_use_id, response)
619                                        .await
620                                    {
621                                        tracing::warn!("Failed to respond to permission request: {}", e);
622                                    }
623                                    continue; // Don't forward to sink
624                                }
625                            }
626                        }
627                        UiMessage::BatchPermissionRequired { batch, .. } => {
628                            // Check if policy handles all requests in the batch
629                            let mut all_handled = true;
630                            let mut approved_grants = Vec::new();
631                            let mut denied_ids = Vec::new();
632
633                            for request in &batch.requests {
634                                match policy_clone.decide(request) {
635                                    PolicyDecision::Allow => {
636                                        // No grant to add, but approved
637                                    }
638                                    PolicyDecision::AllowWithGrant(grant) => {
639                                        approved_grants.push(grant);
640                                    }
641                                    PolicyDecision::Deny { .. } => {
642                                        denied_ids.push(request.id.clone());
643                                    }
644                                    PolicyDecision::AskUser => {
645                                        all_handled = false;
646                                        break;
647                                    }
648                                }
649                            }
650
651                            if all_handled {
652                                // Respond to batch with policy decisions
653                                let response = if denied_ids.is_empty() {
654                                    BatchPermissionResponse::all_granted(&batch.batch_id, approved_grants)
655                                } else {
656                                    BatchPermissionResponse::all_denied(&batch.batch_id, denied_ids)
657                                };
658                                if let Err(e) = permission_registry
659                                    .respond_to_batch(&batch.batch_id, response)
660                                    .await
661                                {
662                                    tracing::warn!("Failed to respond to batch permission request: {}", e);
663                                }
664                                continue; // Don't forward to sink
665                            }
666                            // Fall through to forward to sink if any request needs user input
667                        }
668                        UiMessage::UserInteractionRequired { tool_use_id, .. } => {
669                            if !policy_clone.supports_interaction() {
670                                // Headless mode - auto-cancel the interaction
671                                if let Err(e) = user_interaction_registry.cancel(tool_use_id).await {
672                                    tracing::warn!("Failed to cancel user interaction: {}", e);
673                                }
674                                tracing::debug!("Auto-cancelled user interaction in headless mode");
675                                continue; // Don't forward to sink
676                            }
677                            // Fall through to forward to sink for interactive policies
678                        }
679                        _ => {}
680                    }
681
682                    // Forward event to sink
683                    if let Err(e) = sink_clone.send(event) {
684                        tracing::warn!("Failed to send event to sink: {}", e);
685                    }
686                }
687            });
688        }
689
690        // Create initial session if configured
691        match self.create_initial_session() {
692            Ok((session_id, model, _)) => {
693                tracing::info!(session_id, model = %model, "Created initial session");
694            }
695            Err(e) => {
696                tracing::warn!(error = %e, "No initial session created");
697            }
698        }
699
700        // Run input loop - forward input from source to controller
701        let to_controller_tx = self.to_controller_tx.clone();
702        self.runtime.block_on(async {
703            while let Some(input) = input_source.recv().await {
704                if let Err(e) = to_controller_tx.send(input).await {
705                    tracing::error!(error = %e, "Failed to send input to controller");
706                    break;
707                }
708            }
709        });
710
711        // Shutdown
712        self.shutdown();
713        tracing::info!("{} stopped", self.name);
714
715        Ok(())
716    }
717
718    // ---- Accessors ----
719
720    /// Returns a sender for sending messages to the controller.
721    pub fn to_controller_tx(&self) -> ToControllerTx {
722        self.to_controller_tx.clone()
723    }
724
725    /// Takes the receiver for messages from the controller (can only be called once).
726    pub fn take_from_controller_rx(&mut self) -> Option<FromControllerRx> {
727        self.from_controller_rx.take()
728    }
729
730    /// Returns a reference to the controller.
731    pub fn controller(&self) -> &Arc<LLMController> {
732        &self.controller
733    }
734
735    /// Returns a reference to the runtime.
736    pub fn runtime(&self) -> &Runtime {
737        &self.runtime
738    }
739
740    /// Returns a handle to the runtime.
741    pub fn runtime_handle(&self) -> tokio::runtime::Handle {
742        self.runtime.handle().clone()
743    }
744
745    /// Returns a reference to the user interaction registry.
746    pub fn user_interaction_registry(&self) -> &Arc<UserInteractionRegistry> {
747        &self.user_interaction_registry
748    }
749
750    /// Returns a reference to the permission registry.
751    pub fn permission_registry(&self) -> &Arc<PermissionRegistry> {
752        &self.permission_registry
753    }
754
755    /// Removes a session and cleans up all associated resources.
756    ///
757    /// This is the recommended way to remove a session as it orchestrates cleanup across:
758    /// - The LLM session manager (terminates the session)
759    /// - The permission registry (cancels pending permission requests)
760    /// - The user interaction registry (cancels pending user questions)
761    /// - The tool registry (cleans up per-session state in tools)
762    ///
763    /// # Arguments
764    /// * `session_id` - The ID of the session to remove
765    ///
766    /// # Returns
767    /// true if the session was found and removed, false if session didn't exist
768    pub async fn remove_session(&self, session_id: i64) -> bool {
769        // Remove from controller's session manager
770        let removed = self.controller.remove_session(session_id).await;
771
772        // Clean up pending permission requests for this session
773        self.permission_registry.cancel_session(session_id).await;
774
775        // Clean up pending user interactions for this session
776        self.user_interaction_registry.cancel_session(session_id).await;
777
778        // Clean up per-session state in tools (e.g., bash working directories)
779        self.controller.tool_registry().cleanup_session(session_id).await;
780
781        if removed {
782            tracing::info!(session_id, "Session removed with full cleanup");
783        }
784
785        removed
786    }
787
788    /// Returns a reference to the LLM registry.
789    pub fn llm_registry(&self) -> Option<&LLMRegistry> {
790        self.llm_registry.as_ref()
791    }
792
793    /// Takes the LLM registry (can only be called once).
794    pub fn take_llm_registry(&mut self) -> Option<LLMRegistry> {
795        self.llm_registry.take()
796    }
797
798    /// Returns the cancellation token.
799    pub fn cancel_token(&self) -> CancellationToken {
800        self.cancel_token.clone()
801    }
802
803    /// Returns the agent name.
804    pub fn name(&self) -> &str {
805        &self.name
806    }
807
808    /// Returns a clone of the UI message sender.
809    ///
810    /// This can be used to send messages to the frontend's event loop.
811    pub fn from_controller_tx(&self) -> FromControllerTx {
812        self.from_controller_tx.clone()
813    }
814
815    /// Returns a reference to the tool definitions.
816    pub fn tool_definitions(&self) -> &[LLMTool] {
817        &self.tool_definitions
818    }
819
820    // ---- Skills ----
821
822    /// Returns a reference to the skill registry.
823    pub fn skill_registry(&self) -> &Arc<SkillRegistry> {
824        &self.skill_registry
825    }
826
827    /// Register the ListSkillsTool, allowing the LLM to discover available skills.
828    ///
829    /// This registers the `list_skills` tool with the tool registry and adds its
830    /// definition to the tool list. Call this after `register_tools()` if you want
831    /// the LLM to be able to query available skills.
832    ///
833    /// Returns the LLM tool definition that was added.
834    pub fn register_list_skills_tool(&mut self) -> Result<LLMTool, AgentError> {
835        let tool = ListSkillsTool::new(self.skill_registry.clone());
836        let llm_tool = tool.to_llm_tool();
837
838        self.runtime.block_on(async {
839            self.controller
840                .tool_registry()
841                .register(Arc::new(tool))
842                .await
843        }).map_err(|e| AgentError::ToolRegistration(e.to_string()))?;
844
845        self.tool_definitions.push(llm_tool.clone());
846        tracing::info!("Registered list_skills tool");
847
848        Ok(llm_tool)
849    }
850
851    /// Add a custom skill search path.
852    ///
853    /// Skills are discovered from directories containing SKILL.md files.
854    /// By default, `$PWD/.skills/` and `~/.agent-core/skills/` are searched.
855    pub fn add_skill_path(&mut self, path: std::path::PathBuf) -> &mut Self {
856        self.skill_discovery.add_path(path);
857        self
858    }
859
860    /// Load skills from configured directories.
861    ///
862    /// This scans all configured skill paths and registers discovered skills
863    /// in the skill registry. Call this after configuring skill paths.
864    ///
865    /// Returns the number of skills loaded and any errors encountered.
866    pub fn load_skills(&mut self) -> (usize, Vec<SkillDiscoveryError>) {
867        let results = self.skill_discovery.discover();
868        self.register_discovered_skills(results)
869    }
870
871    /// Load skills from specific paths (one-shot, doesn't modify default discovery).
872    ///
873    /// This creates a temporary discovery instance with only the provided paths,
874    /// loads skills from them, and registers them in the skill registry.
875    /// Unlike `add_skill_path()` + `load_skills()`, this doesn't affect the
876    /// default discovery paths used by `reload_skills()`.
877    ///
878    /// Returns the number of skills loaded and any errors encountered.
879    pub fn load_skills_from(&self, paths: Vec<std::path::PathBuf>) -> (usize, Vec<SkillDiscoveryError>) {
880        let mut discovery = SkillDiscovery::empty();
881        for path in paths {
882            discovery.add_path(path);
883        }
884
885        let results = discovery.discover();
886        self.register_discovered_skills(results)
887    }
888
889    /// Helper to register discovered skills and collect errors.
890    ///
891    /// Logs a warning if a skill with the same name already exists (duplicate detection).
892    fn register_discovered_skills(
893        &self,
894        results: Vec<Result<crate::skills::Skill, SkillDiscoveryError>>,
895    ) -> (usize, Vec<SkillDiscoveryError>) {
896        let mut errors = Vec::new();
897        let mut count = 0;
898
899        for result in results {
900            match result {
901                Ok(skill) => {
902                    let skill_name = skill.metadata.name.clone();
903                    let skill_path = skill.path.clone();
904                    let replaced = self.skill_registry.register(skill);
905
906                    if let Some(old_skill) = replaced {
907                        tracing::warn!(
908                            skill_name = %skill_name,
909                            new_path = %skill_path.display(),
910                            old_path = %old_skill.path.display(),
911                            "Duplicate skill name detected - replaced existing skill"
912                        );
913                    }
914
915                    tracing::info!(
916                        skill_name = %skill_name,
917                        skill_path = %skill_path.display(),
918                        "Loaded skill"
919                    );
920                    count += 1;
921                }
922                Err(e) => {
923                    tracing::warn!(
924                        path = %e.path.display(),
925                        error = %e.message,
926                        "Failed to load skill"
927                    );
928                    errors.push(e);
929                }
930            }
931        }
932
933        tracing::info!("Loaded {} skill(s)", count);
934        (count, errors)
935    }
936
937    /// Reload skills from configured directories.
938    ///
939    /// This re-scans all configured skill paths and updates the registry:
940    /// - New skills are added
941    /// - Removed skills are unregistered
942    /// - Existing skills are re-registered (silently updated)
943    ///
944    /// Returns information about what changed (added/removed only).
945    pub fn reload_skills(&mut self) -> SkillReloadResult {
946        let current_names: std::collections::HashSet<String> =
947            self.skill_registry.names().into_iter().collect();
948
949        let results = self.skill_discovery.discover();
950        let mut discovered_names = std::collections::HashSet::new();
951        let mut result = SkillReloadResult::default();
952
953        // Process discovered skills
954        for discovery_result in results {
955            match discovery_result {
956                Ok(skill) => {
957                    let name = skill.metadata.name.clone();
958                    discovered_names.insert(name.clone());
959
960                    if !current_names.contains(&name) {
961                        tracing::info!(skill_name = %name, "Added new skill");
962                        result.added.push(name);
963                    }
964                    self.skill_registry.register(skill);
965                }
966                Err(e) => {
967                    tracing::warn!(
968                        path = %e.path.display(),
969                        error = %e.message,
970                        "Failed to load skill during reload"
971                    );
972                    result.errors.push(e);
973                }
974            }
975        }
976
977        // Find and remove skills that no longer exist
978        for name in &current_names {
979            if !discovered_names.contains(name) {
980                tracing::info!(skill_name = %name, "Removed skill");
981                self.skill_registry.unregister(name);
982                result.removed.push(name.clone());
983            }
984        }
985
986        tracing::info!(
987            added = result.added.len(),
988            removed = result.removed.len(),
989            errors = result.errors.len(),
990            "Skills reloaded"
991        );
992
993        result
994    }
995
996    /// Get skills XML for injection into system prompts.
997    ///
998    /// Returns an XML string listing all available skills that can be
999    /// included in the system prompt to inform the LLM about available capabilities.
1000    pub fn skills_prompt_xml(&self) -> String {
1001        self.skill_registry.to_prompt_xml()
1002    }
1003
1004    /// Refresh a session's system prompt with current skills.
1005    ///
1006    /// This updates the session's system prompt to include the current
1007    /// `<available_skills>` XML from the skill registry.
1008    ///
1009    /// Note: This appends the skills XML to the existing system prompt.
1010    /// If skills were previously loaded, this may result in duplicate entries.
1011    pub async fn refresh_session_skills(&self, session_id: i64) -> Result<(), AgentError> {
1012        let skills_xml = self.skills_prompt_xml();
1013        if skills_xml.is_empty() {
1014            return Ok(());
1015        }
1016
1017        let session = self
1018            .controller
1019            .get_session(session_id)
1020            .await
1021            .ok_or_else(|| AgentError::SessionNotFound(session_id))?;
1022
1023        let current_prompt = session.system_prompt().await.unwrap_or_default();
1024
1025        // Check if skills are already in the prompt to avoid duplicates
1026        let new_prompt = if current_prompt.contains("<available_skills>") {
1027            // Replace existing skills section
1028            replace_skills_section(&current_prompt, &skills_xml)
1029        } else if current_prompt.is_empty() {
1030            // No existing prompt, just use skills
1031            skills_xml
1032        } else {
1033            // Append skills section
1034            format!("{}\n\n{}", current_prompt, skills_xml)
1035        };
1036
1037        session.set_system_prompt(new_prompt).await;
1038        tracing::debug!(session_id, "Refreshed session skills");
1039        Ok(())
1040    }
1041}
1042
1043/// Replace the <available_skills> section in a system prompt.
1044fn replace_skills_section(prompt: &str, new_skills_xml: &str) -> String {
1045    if let Some(start) = prompt.find("<available_skills>") {
1046        if let Some(end) = prompt.find("</available_skills>") {
1047            let end = end + "</available_skills>".len();
1048            let mut result = String::with_capacity(prompt.len());
1049            result.push_str(&prompt[..start]);
1050            result.push_str(new_skills_xml);
1051            result.push_str(&prompt[end..]);
1052            return result;
1053        }
1054    }
1055    // Fallback: just append
1056    format!("{}\n\n{}", prompt, new_skills_xml)
1057}
1058
1059/// Converts a ControllerEvent to a UiMessage for the frontend.
1060///
1061/// This function maps the internal controller events to UI-friendly messages
1062/// that can be displayed in any frontend (TUI, web, etc.).
1063///
1064/// # Architecture Note
1065///
1066/// This function serves as the **intentional integration point** between the
1067/// controller layer (`ControllerEvent`) and the UI layer (`UiMessage`). It is
1068/// defined in the agent module because:
1069/// 1. The agent orchestrates both controller and UI components
1070/// 2. `UiMessage` is an agent-layer type consumed by frontends
1071/// 3. The agent owns the responsibility of bridging these layers
1072///
1073/// Both `LLMController::send_to_ui()` and `AgentCore` initialization use this
1074/// function to translate controller events into UI-displayable messages.
1075pub fn convert_controller_event_to_ui_message(event: ControllerEvent) -> UiMessage {
1076    match event {
1077        ControllerEvent::StreamStart { session_id, .. } => {
1078            // Silent - don't display stream start messages
1079            UiMessage::System {
1080                session_id,
1081                message: String::new(),
1082            }
1083        }
1084        ControllerEvent::TextChunk {
1085            session_id,
1086            text,
1087            turn_id,
1088        } => UiMessage::TextChunk {
1089            session_id,
1090            turn_id,
1091            text,
1092            input_tokens: 0,
1093            output_tokens: 0,
1094        },
1095        ControllerEvent::ToolUseStart {
1096            session_id,
1097            tool_name,
1098            turn_id,
1099            ..
1100        } => UiMessage::Display {
1101            session_id,
1102            turn_id,
1103            message: format!("Executing tool: {}", tool_name),
1104        },
1105        ControllerEvent::ToolUse {
1106            session_id,
1107            tool,
1108            display_name,
1109            display_title,
1110            turn_id,
1111        } => UiMessage::ToolExecuting {
1112            session_id,
1113            turn_id,
1114            tool_use_id: tool.id.clone(),
1115            display_name: display_name.unwrap_or_else(|| tool.name.clone()),
1116            display_title: display_title.unwrap_or_default(),
1117        },
1118        ControllerEvent::Complete {
1119            session_id,
1120            turn_id,
1121            stop_reason,
1122        } => UiMessage::Complete {
1123            session_id,
1124            turn_id,
1125            input_tokens: 0,
1126            output_tokens: 0,
1127            stop_reason,
1128        },
1129        ControllerEvent::Error {
1130            session_id,
1131            error,
1132            turn_id,
1133        } => UiMessage::Error {
1134            session_id,
1135            turn_id,
1136            error,
1137        },
1138        ControllerEvent::TokenUpdate {
1139            session_id,
1140            input_tokens,
1141            output_tokens,
1142            context_limit,
1143        } => UiMessage::TokenUpdate {
1144            session_id,
1145            turn_id: None,
1146            input_tokens,
1147            output_tokens,
1148            context_limit,
1149        },
1150        ControllerEvent::ToolResult {
1151            session_id,
1152            tool_use_id,
1153            status,
1154            error,
1155            turn_id,
1156            ..
1157        } => UiMessage::ToolCompleted {
1158            session_id,
1159            turn_id,
1160            tool_use_id,
1161            status,
1162            error,
1163        },
1164        ControllerEvent::CommandComplete {
1165            session_id,
1166            command,
1167            success,
1168            message,
1169        } => UiMessage::CommandComplete {
1170            session_id,
1171            command,
1172            success,
1173            message,
1174        },
1175        ControllerEvent::UserInteractionRequired {
1176            session_id,
1177            tool_use_id,
1178            request,
1179            turn_id,
1180        } => UiMessage::UserInteractionRequired {
1181            session_id,
1182            tool_use_id,
1183            request,
1184            turn_id,
1185        },
1186        ControllerEvent::PermissionRequired {
1187            session_id,
1188            tool_use_id,
1189            request,
1190            turn_id,
1191        } => UiMessage::PermissionRequired {
1192            session_id,
1193            tool_use_id,
1194            request,
1195            turn_id,
1196        },
1197        ControllerEvent::BatchPermissionRequired {
1198            session_id,
1199            batch,
1200            turn_id,
1201        } => UiMessage::BatchPermissionRequired {
1202            session_id,
1203            batch,
1204            turn_id,
1205        },
1206    }
1207}
1208
1209#[cfg(test)]
1210mod tests {
1211    use super::*;
1212    use crate::controller::TurnId;
1213
1214    #[test]
1215    fn test_convert_text_chunk_event() {
1216        let event = ControllerEvent::TextChunk {
1217            session_id: 1,
1218            text: "Hello".to_string(),
1219            turn_id: Some(TurnId::new_user_turn(1)),
1220        };
1221
1222        let msg = convert_controller_event_to_ui_message(event);
1223
1224        match msg {
1225            UiMessage::TextChunk {
1226                session_id, text, ..
1227            } => {
1228                assert_eq!(session_id, 1);
1229                assert_eq!(text, "Hello");
1230            }
1231            _ => panic!("Expected TextChunk message"),
1232        }
1233    }
1234
1235    #[test]
1236    fn test_convert_error_event() {
1237        let event = ControllerEvent::Error {
1238            session_id: 1,
1239            error: "Test error".to_string(),
1240            turn_id: None,
1241        };
1242
1243        let msg = convert_controller_event_to_ui_message(event);
1244
1245        match msg {
1246            UiMessage::Error {
1247                session_id, error, ..
1248            } => {
1249                assert_eq!(session_id, 1);
1250                assert_eq!(error, "Test error");
1251            }
1252            _ => panic!("Expected Error message"),
1253        }
1254    }
1255
1256    #[test]
1257    fn test_replace_skills_section_replaces_existing() {
1258        let prompt = "System prompt.\n\n<available_skills>\n  <skill>old</skill>\n</available_skills>\n\nMore text.";
1259        let new_xml = "<available_skills>\n  <skill>new</skill>\n</available_skills>";
1260
1261        let result = replace_skills_section(prompt, new_xml);
1262
1263        assert!(result.contains("<skill>new</skill>"));
1264        assert!(!result.contains("<skill>old</skill>"));
1265        assert!(result.contains("System prompt."));
1266        assert!(result.contains("More text."));
1267    }
1268
1269    #[test]
1270    fn test_replace_skills_section_no_existing() {
1271        let prompt = "System prompt without skills.";
1272        let new_xml = "<available_skills>\n  <skill>new</skill>\n</available_skills>";
1273
1274        let result = replace_skills_section(prompt, new_xml);
1275
1276        // Falls back to appending
1277        assert!(result.contains("System prompt without skills."));
1278        assert!(result.contains("<skill>new</skill>"));
1279    }
1280
1281    #[test]
1282    fn test_replace_skills_section_malformed_no_closing_tag() {
1283        let prompt = "System prompt.\n\n<available_skills>\n  <skill>old</skill>\n\nNo closing tag.";
1284        let new_xml = "<available_skills>\n  <skill>new</skill>\n</available_skills>";
1285
1286        let result = replace_skills_section(prompt, new_xml);
1287
1288        // Falls back to appending since closing tag is missing
1289        assert!(result.contains("<skill>old</skill>"));
1290        assert!(result.contains("<skill>new</skill>"));
1291    }
1292
1293    #[test]
1294    fn test_replace_skills_section_at_end() {
1295        let prompt = "System prompt.\n\n<available_skills>\n  <skill>old</skill>\n</available_skills>";
1296        let new_xml = "<available_skills>\n  <skill>new</skill>\n</available_skills>";
1297
1298        let result = replace_skills_section(prompt, new_xml);
1299
1300        assert!(result.contains("<skill>new</skill>"));
1301        assert!(!result.contains("<skill>old</skill>"));
1302        assert!(result.starts_with("System prompt."));
1303    }
1304}