Skip to main content

everruns_host/
composition.rs

1//! Execution-surface composition for Everruns hosts.
2//!
3//! [`HostComposition`] is what an embedder assembles to decide which
4//! capabilities, LLM drivers and host services a deployment runs with. It
5//! lives here, in the layer that actually executes a turn, rather than in the
6//! kernel: `everruns-core` owns the registries and service contracts, and the
7//! host owns the bundle that selects a deployment's shape (EVE-887).
8//!
9//! Each field is a focused component owned by its own layer — the driver
10//! registry comes from `everruns-provider`, the capability registry from the
11//! neutral capability contract, the egress and utility-LLM services from their
12//! own contracts. This type only carries them together for the runtime; it is
13//! not a registry of registries and adds no vendor branching.
14//!
15//! Product presets stay out of here. Built-in harness provisioning, connectors,
16//! system email and the hosted service catalog are composed by
17//! server/worker/platform code, and inventory-based discovery is confined to
18//! those presets so an embedder can build a composition by hand without
19//! inheriting a product catalog.
20//!
21//! Server-only concerns such as route wiring, auth backends and background task
22//! scheduling stay outside this module so the type can be reused from any
23//! binary crate.
24
25use crate::{DisabledSessionFileSystemFactory, SessionFileSystemFactory};
26use everruns_core::{
27    Capability, CapabilityRegistry, ClassifierService, EgressService, UtilityLlmService,
28    tool_context::ToolContextExtensions,
29};
30use everruns_provider::driver_registry::DriverRegistry;
31use std::sync::{Arc, RwLock};
32
33/// The execution surface a deployment runs with.
34///
35/// `HostComposition` lets an embedder decide which capabilities, LLM drivers
36/// and host services exist at runtime. Server and worker code compose the same
37/// shape so the control plane and execution plane stay aligned.
38///
39/// # Example
40///
41/// ```rust,ignore
42/// use everruns_provider::driver_registry::DriverRegistry;
43/// use everruns_host::HostComposition;
44///
45/// let mut drivers = DriverRegistry::new();
46/// everruns_openai::register_driver(&mut drivers);
47///
48/// let composition = HostComposition::builder()
49///     .driver_registry(drivers)
50///     .capability(everruns_builtins::HumanIntentCapability)
51///     .build();
52/// ```
53pub struct HostComposition {
54    /// Copy-on-write so a capability can be registered through a shared handle
55    /// after composition (EVE-917). Readers take a snapshot and never observe a
56    /// half-built registry; writers clone, mutate, validate, then swap.
57    capability_registry: RwLock<Arc<CapabilityRegistry>>,
58    driver_registry: DriverRegistry,
59    egress_service: Arc<dyn EgressService>,
60    utility_llm_service: Arc<dyn UtilityLlmService>,
61    classifier: Arc<dyn ClassifierService>,
62    session_file_system_factory: Arc<dyn SessionFileSystemFactory>,
63    extensions: ToolContextExtensions,
64}
65
66impl HostComposition {
67    /// Create a composition from explicit registries.
68    pub fn new(capability_registry: CapabilityRegistry, driver_registry: DriverRegistry) -> Self {
69        Self {
70            capability_registry: RwLock::new(Arc::new(capability_registry)),
71            driver_registry,
72            egress_service: Arc::new(everruns_core::DisabledEgressService),
73            utility_llm_service: Arc::new(everruns_core::DisabledUtilityLlmService),
74            classifier: Arc::new(everruns_core::DisabledClassifierService),
75            session_file_system_factory: Arc::new(DisabledSessionFileSystemFactory),
76            extensions: ToolContextExtensions::default(),
77        }
78    }
79
80    /// Create a builder for fluent composition.
81    pub fn builder() -> HostCompositionBuilder {
82        HostCompositionBuilder::new()
83    }
84
85    /// A consistent snapshot of the capability registry.
86    ///
87    /// The snapshot is cheap to hold and never changes underneath its holder,
88    /// so a turn assembled from one either sees a dynamically registered
89    /// capability or does not — never a partially built registry.
90    pub fn capability_registry(&self) -> Arc<CapabilityRegistry> {
91        self.read_registry().clone()
92    }
93
94    /// Register a capability on a live composition (EVE-917).
95    ///
96    /// Callable through a shared handle, so a host holding
97    /// `Arc<InProcessRuntime>` can make a capability discovered mid-session
98    /// known to the runtime. Registration is not activation: the id becomes
99    /// resolvable, and `activate_capability` still decides per-session
100    /// enablement.
101    ///
102    /// Duplicate canonical ids and alias collisions are rejected and leave the
103    /// existing registry untouched.
104    pub fn register_capability(
105        &self,
106        capability: Arc<dyn Capability>,
107    ) -> Result<(), everruns_capability::CapabilityError> {
108        self.update_registry(|registry| registry.try_register_arc(capability))
109    }
110
111    /// Register a capability with composition-time replace semantics.
112    ///
113    /// Re-registering a canonical id overrides the previous implementation,
114    /// matching [`CapabilityRegistry::register_arc`]. Use
115    /// [`HostComposition::register_capability`] for anything registered after
116    /// the deployment is assembled, where a silent override would hide a bug.
117    pub fn register_capability_overriding(&self, capability: Arc<dyn Capability>) {
118        let _ = self.update_registry(|registry| {
119            registry.register_arc(capability);
120            Ok::<(), everruns_capability::CapabilityError>(())
121        });
122    }
123
124    /// Whether a canonical id or alias already resolves in this composition.
125    ///
126    /// Lets a host skip registration for a capability that is already present
127    /// without matching on error strings.
128    pub fn is_capability_registered(&self, id: &str) -> bool {
129        self.read_registry().has(id)
130    }
131
132    fn read_registry(&self) -> std::sync::RwLockReadGuard<'_, Arc<CapabilityRegistry>> {
133        // A panic while a writer holds the lock would poison it. The registry
134        // is still coherent in that case — every write is a swap of a fully
135        // built value — so recovering beats taking the whole runtime down.
136        self.capability_registry
137            .read()
138            .unwrap_or_else(|poisoned| poisoned.into_inner())
139    }
140
141    fn update_registry<E>(
142        &self,
143        mutate: impl FnOnce(&mut CapabilityRegistry) -> Result<(), E>,
144    ) -> Result<(), E> {
145        let mut guard = self
146            .capability_registry
147            .write()
148            .unwrap_or_else(|poisoned| poisoned.into_inner());
149        let mut next = (**guard).clone();
150        mutate(&mut next)?;
151        *guard = Arc::new(next);
152        Ok(())
153    }
154
155    /// Immutable access to the driver registry.
156    pub fn driver_registry(&self) -> &DriverRegistry {
157        &self.driver_registry
158    }
159
160    /// Mutable access to the driver registry.
161    pub fn driver_registry_mut(&mut self) -> &mut DriverRegistry {
162        &mut self.driver_registry
163    }
164
165    /// System-wide outbound network boundary.
166    pub fn egress_service(&self) -> Arc<dyn EgressService> {
167        self.egress_service.clone()
168    }
169
170    /// System-wide utility LLM service for capability internals.
171    pub fn utility_llm_service(&self) -> Arc<dyn UtilityLlmService> {
172        self.utility_llm_service.clone()
173    }
174
175    /// System-wide classifier for capability internals that need typed
176    /// answers rather than text.
177    pub fn classifier(&self) -> Arc<dyn ClassifierService> {
178        self.classifier.clone()
179    }
180
181    /// Factory for the composition-selected session filesystem implementation.
182    pub fn session_file_system_factory(&self) -> Arc<dyn SessionFileSystemFactory> {
183        self.session_file_system_factory.clone()
184    }
185
186    /// Resolve a type-keyed service supplied by a crate layered above core.
187    pub fn extension<T: std::any::Any + Send + Sync>(&self) -> Option<Arc<T>> {
188        self.extensions.get::<T>()
189    }
190}
191
192/// Clones are independent compositions.
193///
194/// They start from the same registry snapshot — cheap, since the snapshot is
195/// shared until one side writes — but a capability registered on a clone is not
196/// visible to the original. That keeps the pre-EVE-917 behaviour of
197/// `#[derive(Clone)]`, where each clone owned its own registry.
198impl Clone for HostComposition {
199    fn clone(&self) -> Self {
200        Self {
201            capability_registry: RwLock::new(self.capability_registry()),
202            driver_registry: self.driver_registry.clone(),
203            egress_service: self.egress_service.clone(),
204            utility_llm_service: self.utility_llm_service.clone(),
205            classifier: self.classifier.clone(),
206            session_file_system_factory: self.session_file_system_factory.clone(),
207            extensions: self.extensions.clone(),
208        }
209    }
210}
211
212impl Default for HostComposition {
213    fn default() -> Self {
214        Self::new(CapabilityRegistry::new(), DriverRegistry::new())
215    }
216}
217
218impl std::fmt::Debug for HostComposition {
219    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
220        f.debug_struct("HostComposition")
221            .field("capabilities", &self.capability_registry())
222            .field("drivers", &self.driver_registry.registered_providers())
223            .field("egress_service", &self.egress_service.name())
224            .field("utility_llm_service", &self.utility_llm_service.name())
225            .field("classifier", &self.classifier.name())
226            .field(
227                "session_file_system_factory",
228                &self.session_file_system_factory.name(),
229            )
230            .field("extensions", &self.extensions)
231            .finish()
232    }
233}
234
235/// Builder for [`HostComposition`].
236pub struct HostCompositionBuilder {
237    composition: HostComposition,
238}
239
240impl HostCompositionBuilder {
241    /// Create a new empty builder.
242    pub fn new() -> Self {
243        Self {
244            composition: HostComposition::default(),
245        }
246    }
247
248    /// Replace the capability registry.
249    pub fn capability_registry(self, registry: CapabilityRegistry) -> Self {
250        *self
251            .composition
252            .capability_registry
253            .write()
254            .unwrap_or_else(|poisoned| poisoned.into_inner()) = Arc::new(registry);
255        self
256    }
257
258    /// Register a capability on the composition.
259    pub fn capability(self, capability: impl Capability + 'static) -> Self {
260        self.composition
261            .register_capability_overriding(Arc::new(capability));
262        self
263    }
264
265    /// Replace the driver registry.
266    pub fn driver_registry(mut self, registry: DriverRegistry) -> Self {
267        self.composition.driver_registry = registry;
268        self
269    }
270
271    /// Set the system-wide outbound egress service.
272    pub fn egress_service(mut self, service: Arc<dyn EgressService>) -> Self {
273        self.composition.egress_service = service;
274        self
275    }
276
277    /// Set the system-wide utility LLM service.
278    pub fn utility_llm_service(mut self, service: Arc<dyn UtilityLlmService>) -> Self {
279        self.composition.utility_llm_service = service;
280        self
281    }
282
283    /// Set the system-wide classifier.
284    pub fn classifier(mut self, service: Arc<dyn ClassifierService>) -> Self {
285        self.composition.classifier = service;
286        self
287    }
288
289    /// Set the host-wide session filesystem factory.
290    pub fn session_file_system_factory(
291        mut self,
292        factory: Arc<dyn SessionFileSystemFactory>,
293    ) -> Self {
294        self.composition.session_file_system_factory = factory;
295        self
296    }
297
298    /// Insert a type-keyed service supplied by a crate layered above core.
299    pub fn extension<T: std::any::Any + Send + Sync>(mut self, value: Arc<T>) -> Self {
300        self.composition.extensions.insert(value);
301        self
302    }
303
304    /// Build the composition.
305    pub fn build(self) -> HostComposition {
306        self.composition
307    }
308}
309
310impl Default for HostCompositionBuilder {
311    fn default() -> Self {
312        Self::new()
313    }
314}
315
316#[cfg(test)]
317mod tests {
318    use super::*;
319    use async_trait::async_trait;
320    use everruns_builtins::HumanIntentCapability;
321    use everruns_core::CapabilityStatus;
322
323    /// Chat driver stub: registration-only, never invoked in these tests.
324    struct StubChatDriver;
325
326    #[async_trait]
327    impl everruns_provider::driver_registry::ChatDriver for StubChatDriver {
328        async fn chat_completion_stream(
329            &self,
330            _endpoint: &everruns_provider::runtime_provider::ProviderEndpoint,
331            _messages: Vec<everruns_provider::driver_registry::LlmMessage>,
332            _config: &everruns_provider::driver_registry::LlmCallConfig,
333        ) -> everruns_provider::error::Result<everruns_provider::driver_registry::LlmResponseStream>
334        {
335            Ok(Box::pin(futures::stream::empty()))
336        }
337    }
338
339    #[test]
340    fn composition_builder_registers_capabilities_and_drivers() {
341        let mut drivers = DriverRegistry::new();
342        let mut descriptor = everruns_provider::driver_registry::DriverDescriptor::chat_only(
343            everruns_provider::provider::DriverId::LlmSim,
344            |_config| {
345                Box::new(StubChatDriver) as everruns_provider::driver_registry::BoxedChatDriver
346            },
347        );
348        descriptor.display_name = "Stub".into();
349        drivers.register_descriptor_or_replace(descriptor);
350
351        let composition = HostComposition::builder()
352            .driver_registry(drivers.clone())
353            .capability(HumanIntentCapability)
354            .build();
355
356        assert!(composition.capability_registry().has("human_intent"));
357        assert!(
358            composition
359                .driver_registry()
360                .has_driver(&everruns_provider::provider::DriverId::LlmSim)
361        );
362    }
363
364    #[test]
365    fn composition_registries_stay_mutable_after_build() {
366        let composition = HostComposition::default();
367        composition.register_capability_overriding(Arc::new(HumanIntentCapability));
368
369        let info = everruns_core::CapabilityInfo::from_core(
370            composition
371                .capability_registry()
372                .get("human_intent")
373                .expect("human_intent registered")
374                .as_ref(),
375        );
376        assert_eq!(info.status, CapabilityStatus::Available);
377    }
378}