Skip to main content

lash_core/plugin/
runtime_impl.rs

1use std::collections::{BTreeMap, BTreeSet};
2use std::sync::Arc;
3use std::sync::{Mutex as StdMutex, Weak};
4
5use super::*;
6
7#[derive(Clone)]
8pub struct PluginHost {
9    factories: Arc<Vec<Arc<dyn PluginFactory>>>,
10    extensions: PluginExtensions,
11    sessions: Arc<StdMutex<BTreeMap<String, Weak<PluginSession>>>>,
12}
13
14struct BuildPluginSessionRequest<'a> {
15    session_id: String,
16    parent_session_id: Option<String>,
17    snapshot: Option<&'a PluginSessionSnapshot>,
18    tool_catalog_overlay: ToolCatalogContribution,
19    tool_snapshot: Option<crate::ToolState>,
20    authority: SessionAuthorityContext,
21}
22
23#[derive(Clone, Debug, Default)]
24pub struct SessionAuthorityContext {
25    pub tool_access: SessionToolAccess,
26    pub subagent: Option<SubagentSessionContext>,
27    pub plugin_options: PluginOptions,
28}
29
30impl PluginHost {
31    pub fn empty() -> Self {
32        Self::new(Vec::new())
33    }
34
35    pub fn new(factories: Vec<Arc<dyn PluginFactory>>) -> Self {
36        let override_ids: BTreeSet<&'static str> =
37            factories.iter().map(|factory| factory.id()).collect();
38        let mut all_factories = super::builtin_plugin_factories();
39        if !override_ids.is_empty() {
40            all_factories.retain(|factory| !override_ids.contains(factory.id()));
41        }
42        all_factories.extend(factories);
43        let extensions = PluginExtensions::from_contributions(
44            all_factories
45                .iter()
46                .flat_map(|factory| factory.extension_contributions()),
47        );
48        Self {
49            factories: Arc::new(all_factories),
50            extensions,
51            sessions: Arc::new(StdMutex::new(BTreeMap::new())),
52        }
53    }
54
55    pub fn with_extensions(mut self, extensions: PluginExtensions) -> Self {
56        self.extensions = extensions;
57        self
58    }
59
60    pub fn isolated_registry(&self) -> Self {
61        Self {
62            factories: Arc::clone(&self.factories),
63            extensions: self.extensions.clone(),
64            sessions: Arc::new(StdMutex::new(BTreeMap::new())),
65        }
66    }
67
68    pub fn extensions(&self) -> &PluginExtensions {
69        &self.extensions
70    }
71
72    pub fn factories(&self) -> &[Arc<dyn PluginFactory>] {
73        self.factories.as_ref().as_slice()
74    }
75
76    /// Ask every factory for its process-engine contributions and register them
77    /// on `runtime_host`, enforcing unique [`ProcessEngine::kind`](crate::ProcessEngine::kind)
78    /// across all engines (directly wired or plugin-contributed).
79    ///
80    /// This is the core-owned installation step that replaces facade-level
81    /// out-of-band wiring: engine construction that needs the fully-built plugin
82    /// host's extensions runs here, after the host is built. The trace context
83    /// handed to factories is the one already on `runtime_host`.
84    pub fn install_process_engine_contributions(
85        &self,
86        mut runtime_host: crate::runtime::RuntimeHostConfig,
87        process_lifecycle_available: bool,
88    ) -> Result<crate::runtime::RuntimeHostConfig, PluginError> {
89        let trace_context = runtime_host.tracing.trace_context.clone();
90        let ctx = super::ProcessEngineContributionContext::new(
91            &self.extensions,
92            &trace_context,
93            process_lifecycle_available,
94        );
95        for factory in self.factories() {
96            for engine in factory.process_engine_contributions(&ctx)? {
97                runtime_host.process_engines = runtime_host
98                    .process_engines
99                    .clone()
100                    .try_with_engine(engine)?;
101            }
102        }
103        Ok(runtime_host)
104    }
105
106    pub fn build_session(
107        &self,
108        session_id: impl Into<String>,
109        snapshot: Option<&PluginSessionSnapshot>,
110    ) -> Result<Arc<PluginSession>, PluginError> {
111        self.build_session_with_overlay(
112            session_id,
113            snapshot,
114            ToolCatalogContribution::default(),
115            None,
116        )
117    }
118
119    /// Variant of [`build_session`] that records the caller as the
120    /// parent of the new session. Plugin factories read
121    /// [`PluginSessionContext::is_root_session`] to gate root-only
122    /// behavior; anything that goes through the plain `build_session`
123    /// is treated as a root session by default.
124    pub fn build_session_with_parent(
125        &self,
126        session_id: impl Into<String>,
127        parent_session_id: Option<String>,
128        snapshot: Option<&PluginSessionSnapshot>,
129        authority: SessionAuthorityContext,
130    ) -> Result<Arc<PluginSession>, PluginError> {
131        self.build_session_with_parent_and_overlay(
132            session_id,
133            parent_session_id,
134            snapshot,
135            ToolCatalogContribution::default(),
136            None,
137            authority,
138        )
139    }
140
141    pub fn build_session_with_parent_and_overlay(
142        &self,
143        session_id: impl Into<String>,
144        parent_session_id: Option<String>,
145        snapshot: Option<&PluginSessionSnapshot>,
146        tool_catalog_overlay: ToolCatalogContribution,
147        tool_snapshot: Option<crate::ToolState>,
148        authority: SessionAuthorityContext,
149    ) -> Result<Arc<PluginSession>, PluginError> {
150        self.build_session_inner(BuildPluginSessionRequest {
151            session_id: session_id.into(),
152            parent_session_id,
153            snapshot,
154            tool_catalog_overlay,
155            tool_snapshot,
156            authority,
157        })
158    }
159
160    pub fn build_session_with_overlay(
161        &self,
162        session_id: impl Into<String>,
163        snapshot: Option<&PluginSessionSnapshot>,
164        tool_catalog_overlay: ToolCatalogContribution,
165        tool_snapshot: Option<crate::ToolState>,
166    ) -> Result<Arc<PluginSession>, PluginError> {
167        self.build_session_inner(BuildPluginSessionRequest {
168            session_id: session_id.into(),
169            parent_session_id: None,
170            snapshot,
171            tool_catalog_overlay,
172            tool_snapshot,
173            authority: SessionAuthorityContext::default(),
174        })
175    }
176
177    fn build_session_inner(
178        &self,
179        request: BuildPluginSessionRequest<'_>,
180    ) -> Result<Arc<PluginSession>, PluginError> {
181        let BuildPluginSessionRequest {
182            session_id,
183            parent_session_id,
184            snapshot,
185            tool_catalog_overlay,
186            tool_snapshot,
187            authority,
188        } = request;
189        let ctx = PluginSessionContext {
190            session_id,
191            tool_access: authority.tool_access.clone(),
192            subagent: authority.subagent.clone(),
193            plugin_options: authority.plugin_options.clone(),
194            extensions: self.extensions.clone(),
195            parent_session_id,
196        };
197        let session_id = ctx.session_id.clone();
198        let mut plugins = Vec::new();
199        let mut reg = PluginRegistrar::new();
200        for factory in self.factories() {
201            let plugin = factory.build(&ctx)?;
202            reg.registering_plugin_id = Some(plugin.id().to_string());
203            plugin.register(&mut reg)?;
204            reg.registering_plugin_id = None;
205            plugins.push(plugin);
206        }
207        let mut contributions = reg.contributions;
208        let protocol_session = contributions.protocol_session.take().ok_or_else(|| {
209            PluginError::Registration("missing protocol session capability".to_string())
210        })?;
211        let protocol_driver = contributions.protocol_driver.take().ok_or_else(|| {
212            PluginError::Registration("missing protocol driver capability".to_string())
213        })?;
214        contributions.protocol_session = Some(protocol_session);
215        contributions.protocol_driver = Some(protocol_driver);
216        contributions
217            .turn_context_transforms
218            .sort_by_key(|entry| std::cmp::Reverse(entry.0));
219        contributions
220            .context_compactors
221            .sort_by_key(|entry| std::cmp::Reverse(entry.0));
222        let triggers = crate::TriggerEventCatalog::from_events(contributions.triggers.clone())
223            .map_err(|message| {
224                PluginError::Registration(format!("invalid trigger event catalog: {message}"))
225            })?;
226        let registry = match tool_snapshot {
227            Some(snapshot) => Arc::new(
228                crate::ToolRegistry::from_tool_providers_with_hidden_tools(
229                    contributions.tool_providers.clone(),
230                    authority.tool_access.hidden_tools.clone(),
231                )
232                .map_err(|err| {
233                    PluginError::Registration(format!("failed to build tool registry: {err}"))
234                })?
235                .fork_with_state(snapshot)
236                .map_err(|err| {
237                    PluginError::Session(format!(
238                        "tool state cannot be applied to this plugin host session: {err}"
239                    ))
240                })?,
241            ),
242            None => Arc::new(
243                crate::ToolRegistry::from_tool_providers_with_hidden_tools(
244                    contributions.tool_providers.clone(),
245                    authority.tool_access.hidden_tools.clone(),
246                )
247                .map_err(|err| {
248                    PluginError::Registration(format!("failed to build tool registry: {err}"))
249                })?,
250            ),
251        };
252        let tools = Arc::clone(&registry) as Arc<dyn ToolProvider>;
253
254        let session = Arc::new(PluginSession {
255            host: self.clone(),
256            session_id: ctx.session_id,
257            plugins,
258            tools,
259            tool_registry: registry,
260            tool_catalog_overlay,
261            tool_access: authority.tool_access,
262            subagent: authority.subagent,
263            extensions: self.extensions.clone(),
264            triggers,
265            contributions,
266        });
267        self.register_session(&session_id, &session)?;
268        let ready = SessionReadyContext {
269            session_id: session.session_id.clone(),
270            host: self.clone(),
271        };
272        for plugin in &session.plugins {
273            plugin.session_ready(ready.clone())?;
274        }
275        if let Some(snapshot) = snapshot {
276            session.restore(snapshot)?;
277        }
278        Ok(session)
279    }
280
281    fn register_session(
282        &self,
283        session_id: &str,
284        session: &Arc<PluginSession>,
285    ) -> Result<(), PluginError> {
286        let mut sessions = self.sessions.lock().map_err(|_| {
287            PluginError::Session("plugin host session registry poisoned".to_string())
288        })?;
289        if let Some(existing) = sessions.get(session_id).and_then(Weak::upgrade) {
290            if !Arc::ptr_eq(&existing, session) {
291                return Err(PluginError::Session(format!(
292                    "session `{session_id}` is already registered on this plugin host"
293                )));
294            }
295            return Ok(());
296        }
297        sessions.insert(session_id.to_string(), Arc::downgrade(session));
298        Ok(())
299    }
300
301    pub fn unregister_session(&self, session_id: &str) -> Result<(), PluginError> {
302        let mut sessions = self.sessions.lock().map_err(|_| {
303            PluginError::Session("plugin host session registry poisoned".to_string())
304        })?;
305        sessions.remove(session_id);
306        Ok(())
307    }
308
309    pub fn session(
310        &self,
311        session_id: &str,
312    ) -> Result<Arc<PluginSession>, PluginOperationInvokeError> {
313        let mut sessions = self
314            .sessions
315            .lock()
316            .map_err(|_| PluginOperationInvokeError::SessionRegistryPoisoned)?;
317        let Some(weak) = sessions.get(session_id).cloned() else {
318            return Err(PluginOperationInvokeError::UnknownSession(
319                session_id.to_string(),
320            ));
321        };
322        match weak.upgrade() {
323            Some(session) => Ok(session),
324            None => {
325                sessions.remove(session_id);
326                Err(PluginOperationInvokeError::UnknownSession(
327                    session_id.to_string(),
328                ))
329            }
330        }
331    }
332}