@@ -181,12 +181,27 @@ pub struct SessionOptions {
181181 /// dispatched locally. The executor is also propagated to sub-agents via
182182 /// the sentinel hook mechanism.
183183 pub hook_executor : Option < Arc < dyn crate :: hooks:: HookExecutor > > ,
184- /// Optional document parser registry for agentic_search tool .
184+ /// Optional document parser registry for document-aware tools .
185185 ///
186- /// When set, enables custom document format support (PDF, Excel, Word, etc.)
187- /// for the agentic_search tool. If not set, only plain text files are searched.
188- pub document_parser_registry :
189- Option < Arc < crate :: tools:: document_parser:: DocumentParserRegistry > > ,
186+ /// When set, enables custom format support (PDF, Excel, Word, etc.)
187+ /// for tools such as `agentic_search` and `agentic_parse`.
188+ /// If not set, only plain text files are accessible to those tools.
189+ pub document_parser_registry : Option < Arc < crate :: document_parser:: DocumentParserRegistry > > ,
190+ /// Plugins to mount onto this session.
191+ ///
192+ /// Each plugin is loaded in order after the core tools are registered.
193+ /// Use [`PluginManager`] or add plugins directly via [`SessionOptions::with_plugin`].
194+ ///
195+ /// # Example
196+ ///
197+ /// ```rust,no_run
198+ /// use a3s_code_core::{SessionOptions, AgenticSearchPlugin, AgenticParsePlugin};
199+ ///
200+ /// let opts = SessionOptions::new()
201+ /// .with_plugin(AgenticSearchPlugin::new())
202+ /// .with_plugin(AgenticParsePlugin::new());
203+ /// ```
204+ pub plugins : Vec < std:: sync:: Arc < dyn crate :: plugin:: Plugin > > ,
190205}
191206
192207impl std:: fmt:: Debug for SessionOptions {
@@ -221,6 +236,10 @@ impl std::fmt::Debug for SessionOptions {
221236 . field ( "auto_compact_threshold" , & self . auto_compact_threshold )
222237 . field ( "continuation_enabled" , & self . continuation_enabled )
223238 . field ( "max_continuation_turns" , & self . max_continuation_turns )
239+ . field (
240+ "plugins" ,
241+ & self . plugins . iter ( ) . map ( |p| p. name ( ) ) . collect :: < Vec < _ > > ( ) ,
242+ )
224243 . field ( "mcp_manager" , & self . mcp_manager . is_some ( ) )
225244 . field ( "temperature" , & self . temperature )
226245 . field ( "thinking_budget" , & self . thinking_budget )
@@ -235,6 +254,15 @@ impl SessionOptions {
235254 Self :: default ( )
236255 }
237256
257+ /// Mount a plugin onto this session.
258+ ///
259+ /// The plugin's tools are registered after the core tools, in the order
260+ /// plugins are added.
261+ pub fn with_plugin ( mut self , plugin : impl crate :: plugin:: Plugin + ' static ) -> Self {
262+ self . plugins . push ( std:: sync:: Arc :: new ( plugin) ) ;
263+ self
264+ }
265+
238266 pub fn with_model ( mut self , model : impl Into < String > ) -> Self {
239267 self . model = Some ( model. into ( ) ) ;
240268 self
@@ -1017,13 +1045,6 @@ impl Agent {
10171045 }
10181046 }
10191047
1020- // Register custom agentic_search tool with document parser registry if provided
1021- if let Some ( ref registry) = opts. document_parser_registry {
1022- use crate :: tools:: AgenticSearchTool ;
1023- let tool = AgenticSearchTool :: with_parser_registry ( ( * * registry) . clone ( ) ) ;
1024- tool_executor. register_dynamic_tool ( Arc :: new ( tool) ) ;
1025- }
1026-
10271048 let tool_defs = tool_executor. definitions ( ) ;
10281049
10291050 // Build prompt slots: start from session options or agent-level config
@@ -1092,6 +1113,37 @@ impl Agent {
10921113 }
10931114 let effective_registry = Arc :: new ( base_registry) ;
10941115
1116+ // Load user-specified plugins — must happen before `skill_prompt` is built
1117+ // so that plugin companion skills appear in the initial system prompt.
1118+ if !opts. plugins . is_empty ( ) {
1119+ use crate :: plugin:: PluginContext ;
1120+ let mut plugin_ctx = PluginContext :: new ( )
1121+ . with_llm ( Arc :: clone ( & self . llm_client ) )
1122+ . with_skill_registry ( Arc :: clone ( & effective_registry) ) ;
1123+ if let Some ( ref dp) = opts. document_parser_registry {
1124+ plugin_ctx = plugin_ctx. with_document_parsers ( Arc :: clone ( dp) ) ;
1125+ }
1126+ let plugin_registry = tool_executor. registry ( ) ;
1127+ for plugin in & opts. plugins {
1128+ tracing:: info!( "Loading plugin '{}' v{}" , plugin. name( ) , plugin. version( ) ) ;
1129+ match plugin. load ( plugin_registry, & plugin_ctx) {
1130+ Ok ( ( ) ) => {
1131+ for skill in plugin. skills ( ) {
1132+ tracing:: debug!(
1133+ "Plugin '{}' registered skill '{}'" ,
1134+ plugin. name( ) ,
1135+ skill. name
1136+ ) ;
1137+ effective_registry. register_unchecked ( skill) ;
1138+ }
1139+ }
1140+ Err ( e) => {
1141+ tracing:: error!( "Plugin '{}' failed to load: {}" , plugin. name( ) , e) ;
1142+ }
1143+ }
1144+ }
1145+ }
1146+
10951147 // Append skill directory listing to the extra prompt slot
10961148 let skill_prompt = effective_registry. to_system_prompt ( ) ;
10971149 if !skill_prompt. is_empty ( ) {
@@ -1235,6 +1287,11 @@ impl Agent {
12351287 }
12361288 tool_context = tool_context. with_agent_event_tx ( agent_event_tx) ;
12371289
1290+ // Wire document parser registry so all tools can access it via ToolContext.
1291+ if let Some ( ref registry) = opts. document_parser_registry {
1292+ tool_context = tool_context. with_document_parsers ( Arc :: clone ( registry) ) ;
1293+ }
1294+
12381295 // Wire sandbox when a concrete handle is provided by the host application.
12391296 if let Some ( handle) = opts. sandbox_handle . clone ( ) {
12401297 tool_executor. registry ( ) . set_sandbox ( Arc :: clone ( & handle) ) ;
0 commit comments