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, 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    session_file_system_factory: Arc<dyn SessionFileSystemFactory>,
62    extensions: ToolContextExtensions,
63}
64
65impl HostComposition {
66    /// Create a composition from explicit registries.
67    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    /// Create a builder for fluent composition.
79    pub fn builder() -> HostCompositionBuilder {
80        HostCompositionBuilder::new()
81    }
82
83    /// A consistent snapshot of the capability registry.
84    ///
85    /// The snapshot is cheap to hold and never changes underneath its holder,
86    /// so a turn assembled from one either sees a dynamically registered
87    /// capability or does not — never a partially built registry.
88    pub fn capability_registry(&self) -> Arc<CapabilityRegistry> {
89        self.read_registry().clone()
90    }
91
92    /// Register a capability on a live composition (EVE-917).
93    ///
94    /// Callable through a shared handle, so a host holding
95    /// `Arc<InProcessRuntime>` can make a capability discovered mid-session
96    /// known to the runtime. Registration is not activation: the id becomes
97    /// resolvable, and `activate_capability` still decides per-session
98    /// enablement.
99    ///
100    /// Duplicate canonical ids and alias collisions are rejected and leave the
101    /// existing registry untouched.
102    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    /// Register a capability with composition-time replace semantics.
110    ///
111    /// Re-registering a canonical id overrides the previous implementation,
112    /// matching [`CapabilityRegistry::register_arc`]. Use
113    /// [`HostComposition::register_capability`] for anything registered after
114    /// the deployment is assembled, where a silent override would hide a bug.
115    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    /// Whether a canonical id or alias already resolves in this composition.
123    ///
124    /// Lets a host skip registration for a capability that is already present
125    /// without matching on error strings.
126    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        // A panic while a writer holds the lock would poison it. The registry
132        // is still coherent in that case — every write is a swap of a fully
133        // built value — so recovering beats taking the whole runtime down.
134        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    /// Immutable access to the driver registry.
154    pub fn driver_registry(&self) -> &DriverRegistry {
155        &self.driver_registry
156    }
157
158    /// Mutable access to the driver registry.
159    pub fn driver_registry_mut(&mut self) -> &mut DriverRegistry {
160        &mut self.driver_registry
161    }
162
163    /// System-wide outbound network boundary.
164    pub fn egress_service(&self) -> Arc<dyn EgressService> {
165        self.egress_service.clone()
166    }
167
168    /// System-wide utility LLM service for capability internals.
169    pub fn utility_llm_service(&self) -> Arc<dyn UtilityLlmService> {
170        self.utility_llm_service.clone()
171    }
172
173    /// Factory for the composition-selected session filesystem implementation.
174    pub fn session_file_system_factory(&self) -> Arc<dyn SessionFileSystemFactory> {
175        self.session_file_system_factory.clone()
176    }
177
178    /// Resolve a type-keyed service supplied by a crate layered above core.
179    pub fn extension<T: std::any::Any + Send + Sync>(&self) -> Option<Arc<T>> {
180        self.extensions.get::<T>()
181    }
182}
183
184/// Clones are independent compositions.
185///
186/// They start from the same registry snapshot — cheap, since the snapshot is
187/// shared until one side writes — but a capability registered on a clone is not
188/// visible to the original. That keeps the pre-EVE-917 behaviour of
189/// `#[derive(Clone)]`, where each clone owned its own registry.
190impl 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
225/// Builder for [`HostComposition`].
226pub struct HostCompositionBuilder {
227    composition: HostComposition,
228}
229
230impl HostCompositionBuilder {
231    /// Create a new empty builder.
232    pub fn new() -> Self {
233        Self {
234            composition: HostComposition::default(),
235        }
236    }
237
238    /// Replace the capability registry.
239    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    /// Register a capability on the composition.
249    pub fn capability(self, capability: impl Capability + 'static) -> Self {
250        self.composition
251            .register_capability_overriding(Arc::new(capability));
252        self
253    }
254
255    /// Replace the driver registry.
256    pub fn driver_registry(mut self, registry: DriverRegistry) -> Self {
257        self.composition.driver_registry = registry;
258        self
259    }
260
261    /// Set the system-wide outbound egress service.
262    pub fn egress_service(mut self, service: Arc<dyn EgressService>) -> Self {
263        self.composition.egress_service = service;
264        self
265    }
266
267    /// Set the system-wide utility LLM service.
268    pub fn utility_llm_service(mut self, service: Arc<dyn UtilityLlmService>) -> Self {
269        self.composition.utility_llm_service = service;
270        self
271    }
272
273    /// Set the host-wide session filesystem factory.
274    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    /// Insert a type-keyed service supplied by a crate layered above core.
283    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    /// Build the composition.
289    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    /// Chat driver stub: registration-only, never invoked in these tests.
308    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}