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    lashlang_abilities: lashlang::LashlangAbilities,
11    lashlang_language_features: lashlang::LashlangLanguageFeatures,
12    lashlang_resources: lashlang::ResourceCatalog,
13    sessions: Arc<StdMutex<BTreeMap<String, Weak<PluginSession>>>>,
14}
15
16struct BuildPluginSessionRequest<'a> {
17    session_id: String,
18    parent_session_id: Option<String>,
19    snapshot: Option<&'a PluginSessionSnapshot>,
20    tool_surface_overlay: ToolSurfaceContribution,
21    tool_snapshot: Option<crate::ToolState>,
22    authority: SessionAuthorityContext,
23}
24
25#[derive(Clone, Debug, Default)]
26pub struct SessionAuthorityContext {
27    pub tool_access: SessionToolAccess,
28    pub subagent: Option<SubagentSessionContext>,
29    pub plugin_options: PluginOptions,
30}
31
32impl PluginHost {
33    pub fn empty() -> Self {
34        Self::new(Vec::new())
35    }
36
37    pub fn new(factories: Vec<Arc<dyn PluginFactory>>) -> Self {
38        let override_ids: BTreeSet<&'static str> =
39            factories.iter().map(|factory| factory.id()).collect();
40        let mut all_factories = super::builtin_plugin_factories();
41        if !override_ids.is_empty() {
42            all_factories.retain(|factory| !override_ids.contains(factory.id()));
43        }
44        all_factories.extend(factories);
45        let lashlang_abilities = all_factories.iter().fold(
46            lashlang::LashlangAbilities::default(),
47            |abilities, factory| abilities.union(factory.lashlang_abilities()),
48        );
49        let lashlang_language_features = all_factories.iter().fold(
50            lashlang::LashlangLanguageFeatures::default(),
51            |features, factory| features.union(factory.lashlang_language_features()),
52        );
53        let lashlang_resources = all_factories.iter().fold(
54            lashlang::ResourceCatalog::new(),
55            |mut resources, factory| {
56                resources.extend(factory.lashlang_resources());
57                resources
58            },
59        );
60        Self {
61            factories: Arc::new(all_factories),
62            lashlang_abilities,
63            lashlang_language_features,
64            lashlang_resources,
65            sessions: Arc::new(StdMutex::new(BTreeMap::new())),
66        }
67    }
68
69    pub fn with_lashlang_abilities(mut self, abilities: lashlang::LashlangAbilities) -> Self {
70        self.lashlang_abilities = abilities;
71        self
72    }
73
74    pub fn with_lashlang_language_features(
75        mut self,
76        language_features: lashlang::LashlangLanguageFeatures,
77    ) -> Self {
78        self.lashlang_language_features = language_features;
79        self
80    }
81
82    pub fn with_lashlang_resources(mut self, resources: lashlang::ResourceCatalog) -> Self {
83        self.lashlang_resources = resources;
84        self
85    }
86
87    pub fn isolated_registry(&self) -> Self {
88        Self {
89            factories: Arc::clone(&self.factories),
90            lashlang_abilities: self.lashlang_abilities,
91            lashlang_language_features: self.lashlang_language_features,
92            lashlang_resources: self.lashlang_resources.clone(),
93            sessions: Arc::new(StdMutex::new(BTreeMap::new())),
94        }
95    }
96
97    pub fn lashlang_abilities(&self) -> lashlang::LashlangAbilities {
98        self.lashlang_abilities
99    }
100
101    pub fn lashlang_language_features(&self) -> lashlang::LashlangLanguageFeatures {
102        self.lashlang_language_features
103    }
104
105    pub fn lashlang_resources(&self) -> lashlang::ResourceCatalog {
106        self.lashlang_resources.clone()
107    }
108
109    pub fn factories(&self) -> &[Arc<dyn PluginFactory>] {
110        self.factories.as_ref().as_slice()
111    }
112
113    pub fn build_session(
114        &self,
115        session_id: impl Into<String>,
116        snapshot: Option<&PluginSessionSnapshot>,
117    ) -> Result<Arc<PluginSession>, PluginError> {
118        self.build_session_with_surface(
119            session_id,
120            snapshot,
121            ToolSurfaceContribution::default(),
122            None,
123        )
124    }
125
126    /// Variant of [`build_session`] that records the caller as the
127    /// parent of the new session. Plugin factories read
128    /// [`PluginSessionContext::is_root_session`] to gate root-only
129    /// behavior; anything that goes through the plain `build_session`
130    /// is treated as a root session by default.
131    pub fn build_session_with_parent(
132        &self,
133        session_id: impl Into<String>,
134        parent_session_id: Option<String>,
135        snapshot: Option<&PluginSessionSnapshot>,
136        authority: SessionAuthorityContext,
137    ) -> Result<Arc<PluginSession>, PluginError> {
138        self.build_session_with_parent_and_surface(
139            session_id,
140            parent_session_id,
141            snapshot,
142            ToolSurfaceContribution::default(),
143            None,
144            authority,
145        )
146    }
147
148    pub fn build_session_with_parent_and_surface(
149        &self,
150        session_id: impl Into<String>,
151        parent_session_id: Option<String>,
152        snapshot: Option<&PluginSessionSnapshot>,
153        tool_surface_overlay: ToolSurfaceContribution,
154        tool_snapshot: Option<crate::ToolState>,
155        authority: SessionAuthorityContext,
156    ) -> Result<Arc<PluginSession>, PluginError> {
157        self.build_session_inner(BuildPluginSessionRequest {
158            session_id: session_id.into(),
159            parent_session_id,
160            snapshot,
161            tool_surface_overlay,
162            tool_snapshot,
163            authority,
164        })
165    }
166
167    pub fn build_session_with_surface(
168        &self,
169        session_id: impl Into<String>,
170        snapshot: Option<&PluginSessionSnapshot>,
171        tool_surface_overlay: ToolSurfaceContribution,
172        tool_snapshot: Option<crate::ToolState>,
173    ) -> Result<Arc<PluginSession>, PluginError> {
174        self.build_session_inner(BuildPluginSessionRequest {
175            session_id: session_id.into(),
176            parent_session_id: None,
177            snapshot,
178            tool_surface_overlay,
179            tool_snapshot,
180            authority: SessionAuthorityContext::default(),
181        })
182    }
183
184    fn build_session_inner(
185        &self,
186        request: BuildPluginSessionRequest<'_>,
187    ) -> Result<Arc<PluginSession>, PluginError> {
188        let BuildPluginSessionRequest {
189            session_id,
190            parent_session_id,
191            snapshot,
192            tool_surface_overlay,
193            tool_snapshot,
194            authority,
195        } = request;
196        let ctx = PluginSessionContext {
197            session_id,
198            tool_access: authority.tool_access.clone(),
199            subagent: authority.subagent.clone(),
200            plugin_options: authority.plugin_options.clone(),
201            lashlang_abilities: self.lashlang_abilities,
202            lashlang_language_features: self.lashlang_language_features,
203            parent_session_id,
204        };
205        let session_id = ctx.session_id.clone();
206        let mut tool_snapshot = tool_snapshot;
207        if let Some(snapshot) = &mut tool_snapshot {
208            let hidden_tools = &authority.tool_access.hidden_tools;
209            if !hidden_tools.is_empty() {
210                snapshot.retain(|name, _| !hidden_tools.contains(name));
211            }
212        }
213        let mut plugins = Vec::new();
214        let mut reg = PluginRegistrar::new();
215        for factory in self.factories() {
216            let plugin = factory.build(&ctx)?;
217            reg.registering_plugin_id = Some(plugin.id().to_string());
218            plugin.register(&mut reg)?;
219            reg.registering_plugin_id = None;
220            plugins.push(plugin);
221        }
222        let mut contributions = reg.contributions;
223        let protocol_session = contributions.protocol_session.take().ok_or_else(|| {
224            PluginError::Registration("missing protocol session capability".to_string())
225        })?;
226        let protocol_driver = contributions.protocol_driver.take().ok_or_else(|| {
227            PluginError::Registration("missing protocol driver capability".to_string())
228        })?;
229        contributions.protocol_session = Some(protocol_session);
230        contributions.protocol_driver = Some(protocol_driver);
231        contributions
232            .turn_context_transforms
233            .sort_by_key(|entry| std::cmp::Reverse(entry.0));
234        contributions
235            .context_compactors
236            .sort_by_key(|entry| std::cmp::Reverse(entry.0));
237        let host_events = crate::HostEventCatalog::from_events(contributions.host_events.clone())
238            .map_err(|message| {
239            PluginError::Registration(format!("invalid host event catalog: {message}"))
240        })?;
241        let mut lashlang_resources = self.lashlang_resources.clone();
242        for event in host_events.events() {
243            lashlang_resources
244                .add_trigger_source_constructor(
245                    event.source_type().split('.'),
246                    lashlang::TypeExpr::Object(Vec::new()),
247                    event.payload_type().clone(),
248                )
249                .map_err(|err| {
250                    PluginError::Registration(format!(
251                        "invalid host event trigger source `{}.{}`: {err}",
252                        event.alias, event.event
253                    ))
254                })?;
255        }
256        let registry = match tool_snapshot {
257            Some(snapshot) => Arc::new(
258                crate::ToolRegistry::from_tool_providers(contributions.tool_providers.clone())
259                    .map_err(|err| {
260                        PluginError::Registration(format!("failed to build tool registry: {err}"))
261                    })?
262                    .fork_with_state(snapshot)
263                    .map_err(|err| {
264                        PluginError::Session(format!(
265                            "tool state cannot be applied to this plugin host session: {err}"
266                        ))
267                    })?,
268            ),
269            None => Arc::new(
270                crate::ToolRegistry::from_tool_providers(contributions.tool_providers.clone())
271                    .map_err(|err| {
272                        PluginError::Registration(format!("failed to build tool registry: {err}"))
273                    })?,
274            ),
275        };
276        let tools = Arc::clone(&registry) as Arc<dyn ToolProvider>;
277
278        let session = Arc::new(PluginSession {
279            host: self.clone(),
280            session_id: ctx.session_id,
281            plugins,
282            tools,
283            tool_registry: registry,
284            tool_surface_overlay,
285            tool_access: authority.tool_access,
286            subagent: authority.subagent,
287            lashlang_abilities: self.lashlang_abilities,
288            lashlang_language_features: self.lashlang_language_features,
289            lashlang_resources,
290            host_events,
291            contributions,
292        });
293        self.register_session(&session_id, &session)?;
294        let ready = SessionReadyContext {
295            session_id: session.session_id.clone(),
296            host: self.clone(),
297        };
298        for plugin in &session.plugins {
299            plugin.session_ready(ready.clone())?;
300        }
301        if let Some(snapshot) = snapshot {
302            session.restore(snapshot)?;
303        }
304        Ok(session)
305    }
306
307    pub async fn invoke_plugin_action_sessionless(
308        &self,
309        name: &str,
310        args: serde_json::Value,
311    ) -> Result<ToolResult, PluginError> {
312        let session = self.build_session(
313            format!("__external__-{}", uuid::Uuid::new_v4().simple()),
314            None,
315        )?;
316        session
317            .invoke_plugin_action(
318                name,
319                args,
320                None,
321                false,
322                Arc::new(NoopSessionManager),
323                Arc::new(NoopSessionManager),
324                Arc::new(NoopSessionManager),
325                Arc::new(crate::UnavailableProcessService),
326            )
327            .await
328            .map_err(|err| PluginError::Invoke(err.to_string()))
329    }
330
331    fn register_session(
332        &self,
333        session_id: &str,
334        session: &Arc<PluginSession>,
335    ) -> Result<(), PluginError> {
336        let mut sessions = self.sessions.lock().map_err(|_| {
337            PluginError::Session("plugin host session registry poisoned".to_string())
338        })?;
339        if let Some(existing) = sessions.get(session_id).and_then(Weak::upgrade) {
340            if !Arc::ptr_eq(&existing, session) {
341                return Err(PluginError::Session(format!(
342                    "session `{session_id}` is already registered on this plugin host"
343                )));
344            }
345            return Ok(());
346        }
347        sessions.insert(session_id.to_string(), Arc::downgrade(session));
348        Ok(())
349    }
350
351    pub fn unregister_session(&self, session_id: &str) -> Result<(), PluginError> {
352        let mut sessions = self.sessions.lock().map_err(|_| {
353            PluginError::Session("plugin host session registry poisoned".to_string())
354        })?;
355        sessions.remove(session_id);
356        Ok(())
357    }
358
359    pub fn session(&self, session_id: &str) -> Result<Arc<PluginSession>, PluginActionInvokeError> {
360        let mut sessions = self
361            .sessions
362            .lock()
363            .map_err(|_| PluginActionInvokeError::SessionRegistryPoisoned)?;
364        let Some(weak) = sessions.get(session_id).cloned() else {
365            return Err(PluginActionInvokeError::UnknownSession(
366                session_id.to_string(),
367            ));
368        };
369        match weak.upgrade() {
370            Some(session) => Ok(session),
371            None => {
372                sessions.remove(session_id);
373                Err(PluginActionInvokeError::UnknownSession(
374                    session_id.to_string(),
375                ))
376            }
377        }
378    }
379
380    #[expect(
381        clippy::too_many_arguments,
382        reason = "host action invocation wires the runtime service bundle at the plugin boundary"
383    )]
384    pub async fn invoke_plugin_action_for_session(
385        &self,
386        session_id: &str,
387        name: &str,
388        args: serde_json::Value,
389        sessions: Arc<dyn SessionStateService>,
390        session_lifecycle: Arc<dyn SessionLifecycleService>,
391        session_graph: Arc<dyn SessionGraphService>,
392        processes: Arc<dyn crate::ProcessService>,
393    ) -> Result<ToolResult, PluginActionInvokeError> {
394        let session = self.session(session_id)?;
395        session
396            .invoke_plugin_action(
397                name,
398                args,
399                Some(session_id.to_string()),
400                false,
401                sessions,
402                session_lifecycle,
403                session_graph,
404                processes,
405            )
406            .await
407    }
408}