everruns_host/
composition.rs1use crate::{DisabledSessionFileSystemFactory, SessionFileSystemFactory};
26use everruns_core::{
27 Capability, CapabilityRegistry, EgressService, UtilityLlmService,
28 tool_context::ToolContextExtensions,
29};
30use everruns_provider::driver_registry::DriverRegistry;
31use std::sync::{Arc, RwLock};
32
33pub struct HostComposition {
54 capability_registry: RwLock<Arc<CapabilityRegistry>>,
58 driver_registry: DriverRegistry,
59 egress_service: Arc<dyn EgressService>,
60 utility_llm_service: Arc<dyn UtilityLlmService>,
61 session_file_system_factory: Arc<dyn SessionFileSystemFactory>,
62 extensions: ToolContextExtensions,
63}
64
65impl HostComposition {
66 pub fn new(capability_registry: CapabilityRegistry, driver_registry: DriverRegistry) -> Self {
68 Self {
69 capability_registry: RwLock::new(Arc::new(capability_registry)),
70 driver_registry,
71 egress_service: Arc::new(everruns_core::DisabledEgressService),
72 utility_llm_service: Arc::new(everruns_core::DisabledUtilityLlmService),
73 session_file_system_factory: Arc::new(DisabledSessionFileSystemFactory),
74 extensions: ToolContextExtensions::default(),
75 }
76 }
77
78 pub fn builder() -> HostCompositionBuilder {
80 HostCompositionBuilder::new()
81 }
82
83 pub fn capability_registry(&self) -> Arc<CapabilityRegistry> {
89 self.read_registry().clone()
90 }
91
92 pub fn register_capability(
103 &self,
104 capability: Arc<dyn Capability>,
105 ) -> Result<(), everruns_capability::CapabilityError> {
106 self.update_registry(|registry| registry.try_register_arc(capability))
107 }
108
109 pub fn register_capability_overriding(&self, capability: Arc<dyn Capability>) {
116 let _ = self.update_registry(|registry| {
117 registry.register_arc(capability);
118 Ok::<(), everruns_capability::CapabilityError>(())
119 });
120 }
121
122 pub fn is_capability_registered(&self, id: &str) -> bool {
127 self.read_registry().has(id)
128 }
129
130 fn read_registry(&self) -> std::sync::RwLockReadGuard<'_, Arc<CapabilityRegistry>> {
131 self.capability_registry
135 .read()
136 .unwrap_or_else(|poisoned| poisoned.into_inner())
137 }
138
139 fn update_registry<E>(
140 &self,
141 mutate: impl FnOnce(&mut CapabilityRegistry) -> Result<(), E>,
142 ) -> Result<(), E> {
143 let mut guard = self
144 .capability_registry
145 .write()
146 .unwrap_or_else(|poisoned| poisoned.into_inner());
147 let mut next = (**guard).clone();
148 mutate(&mut next)?;
149 *guard = Arc::new(next);
150 Ok(())
151 }
152
153 pub fn driver_registry(&self) -> &DriverRegistry {
155 &self.driver_registry
156 }
157
158 pub fn driver_registry_mut(&mut self) -> &mut DriverRegistry {
160 &mut self.driver_registry
161 }
162
163 pub fn egress_service(&self) -> Arc<dyn EgressService> {
165 self.egress_service.clone()
166 }
167
168 pub fn utility_llm_service(&self) -> Arc<dyn UtilityLlmService> {
170 self.utility_llm_service.clone()
171 }
172
173 pub fn session_file_system_factory(&self) -> Arc<dyn SessionFileSystemFactory> {
175 self.session_file_system_factory.clone()
176 }
177
178 pub fn extension<T: std::any::Any + Send + Sync>(&self) -> Option<Arc<T>> {
180 self.extensions.get::<T>()
181 }
182}
183
184impl Clone for HostComposition {
191 fn clone(&self) -> Self {
192 Self {
193 capability_registry: RwLock::new(self.capability_registry()),
194 driver_registry: self.driver_registry.clone(),
195 egress_service: self.egress_service.clone(),
196 utility_llm_service: self.utility_llm_service.clone(),
197 session_file_system_factory: self.session_file_system_factory.clone(),
198 extensions: self.extensions.clone(),
199 }
200 }
201}
202
203impl Default for HostComposition {
204 fn default() -> Self {
205 Self::new(CapabilityRegistry::new(), DriverRegistry::new())
206 }
207}
208
209impl std::fmt::Debug for HostComposition {
210 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
211 f.debug_struct("HostComposition")
212 .field("capabilities", &self.capability_registry())
213 .field("drivers", &self.driver_registry.registered_providers())
214 .field("egress_service", &self.egress_service.name())
215 .field("utility_llm_service", &self.utility_llm_service.name())
216 .field(
217 "session_file_system_factory",
218 &self.session_file_system_factory.name(),
219 )
220 .field("extensions", &self.extensions)
221 .finish()
222 }
223}
224
225pub struct HostCompositionBuilder {
227 composition: HostComposition,
228}
229
230impl HostCompositionBuilder {
231 pub fn new() -> Self {
233 Self {
234 composition: HostComposition::default(),
235 }
236 }
237
238 pub fn capability_registry(self, registry: CapabilityRegistry) -> Self {
240 *self
241 .composition
242 .capability_registry
243 .write()
244 .unwrap_or_else(|poisoned| poisoned.into_inner()) = Arc::new(registry);
245 self
246 }
247
248 pub fn capability(self, capability: impl Capability + 'static) -> Self {
250 self.composition
251 .register_capability_overriding(Arc::new(capability));
252 self
253 }
254
255 pub fn driver_registry(mut self, registry: DriverRegistry) -> Self {
257 self.composition.driver_registry = registry;
258 self
259 }
260
261 pub fn egress_service(mut self, service: Arc<dyn EgressService>) -> Self {
263 self.composition.egress_service = service;
264 self
265 }
266
267 pub fn utility_llm_service(mut self, service: Arc<dyn UtilityLlmService>) -> Self {
269 self.composition.utility_llm_service = service;
270 self
271 }
272
273 pub fn session_file_system_factory(
275 mut self,
276 factory: Arc<dyn SessionFileSystemFactory>,
277 ) -> Self {
278 self.composition.session_file_system_factory = factory;
279 self
280 }
281
282 pub fn extension<T: std::any::Any + Send + Sync>(mut self, value: Arc<T>) -> Self {
284 self.composition.extensions.insert(value);
285 self
286 }
287
288 pub fn build(self) -> HostComposition {
290 self.composition
291 }
292}
293
294impl Default for HostCompositionBuilder {
295 fn default() -> Self {
296 Self::new()
297 }
298}
299
300#[cfg(test)]
301mod tests {
302 use super::*;
303 use async_trait::async_trait;
304 use everruns_builtins::HumanIntentCapability;
305 use everruns_core::CapabilityStatus;
306
307 struct StubChatDriver;
309
310 #[async_trait]
311 impl everruns_provider::driver_registry::ChatDriver for StubChatDriver {
312 async fn chat_completion_stream(
313 &self,
314 _endpoint: &everruns_provider::runtime_provider::ProviderEndpoint,
315 _messages: Vec<everruns_provider::driver_registry::LlmMessage>,
316 _config: &everruns_provider::driver_registry::LlmCallConfig,
317 ) -> everruns_provider::error::Result<everruns_provider::driver_registry::LlmResponseStream>
318 {
319 Ok(Box::pin(futures::stream::empty()))
320 }
321 }
322
323 #[test]
324 fn composition_builder_registers_capabilities_and_drivers() {
325 let mut drivers = DriverRegistry::new();
326 let mut descriptor = everruns_provider::driver_registry::DriverDescriptor::chat_only(
327 everruns_provider::provider::DriverId::LlmSim,
328 |_config| {
329 Box::new(StubChatDriver) as everruns_provider::driver_registry::BoxedChatDriver
330 },
331 );
332 descriptor.display_name = "Stub".into();
333 drivers.register_descriptor_or_replace(descriptor);
334
335 let composition = HostComposition::builder()
336 .driver_registry(drivers.clone())
337 .capability(HumanIntentCapability)
338 .build();
339
340 assert!(composition.capability_registry().has("human_intent"));
341 assert!(
342 composition
343 .driver_registry()
344 .has_driver(&everruns_provider::provider::DriverId::LlmSim)
345 );
346 }
347
348 #[test]
349 fn composition_registries_stay_mutable_after_build() {
350 let composition = HostComposition::default();
351 composition.register_capability_overriding(Arc::new(HumanIntentCapability));
352
353 let info = everruns_core::CapabilityInfo::from_core(
354 composition
355 .capability_registry()
356 .get("human_intent")
357 .expect("human_intent registered")
358 .as_ref(),
359 );
360 assert_eq!(info.status, CapabilityStatus::Available);
361 }
362}