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 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 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(®istry) 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}