Skip to main content

lash_core/runtime/
host.rs

1use lash_trace::{TraceContext, TraceLevel, TraceSink};
2use std::sync::Arc;
3
4use super::process::{
5    InMemoryProcessExecutionEnvStore, ProcessEngineRegistry, ProcessExecutionEnvStore,
6    ProcessRegistry,
7};
8use super::{
9    EffectHost, InlineEffectHost, ProcessWorkDriver, QueuedWorkDriver, SessionStoreFactory,
10    TerminationPolicy,
11};
12
13/// Required host configuration for all runtimes.
14#[derive(Clone)]
15pub struct RuntimeHostConfig {
16    pub durability: RuntimeDurabilityConfig,
17    pub process_engines: ProcessEngineRegistry,
18    pub providers: RuntimeProviderConfig,
19    pub prompt: RuntimePromptConfig,
20    pub control: RuntimeControlConfig,
21    pub tracing: RuntimeTracingConfig,
22    /// Injected time source. Durable timestamps and timeout/backoff logic read
23    /// this rather than the OS clock directly, so replay is reproducible and
24    /// tests can drive time. Defaults to [`SystemClock`](super::SystemClock).
25    pub clock: Arc<dyn super::Clock>,
26}
27
28#[derive(Clone)]
29pub struct RuntimeDurabilityConfig {
30    /// The session-bound attachment facade every runtime consumer sees. Hosts
31    /// supply a flat [`AttachmentStore`](crate::AttachmentStore) backend
32    /// (`RuntimeHostConfig::new`, the builder); the runtime wraps it here in a
33    /// [`SessionAttachmentStore`](crate::SessionAttachmentStore) and rebinds it
34    /// to the live session (with a reference-tracking manifest) at session
35    /// start. Before rebinding it is an ephemeral facade with no boundary guard.
36    pub attachment_store: Arc<crate::SessionAttachmentStore>,
37    pub process_env_store: Arc<dyn ProcessExecutionEnvStore>,
38}
39
40#[derive(Clone)]
41pub struct RuntimeProviderConfig {
42    pub provider_resolver: Arc<dyn crate::RuntimeProviderResolver>,
43}
44
45#[derive(Clone)]
46pub struct RuntimePromptConfig {
47    pub prompt: crate::PromptLayer,
48}
49
50#[derive(Clone)]
51pub struct RuntimeControlConfig {
52    pub effect_host: Arc<dyn EffectHost>,
53    pub process_cancel_ability: Arc<dyn crate::ProcessCancelAbility>,
54    pub termination: TerminationPolicy,
55    /// Lease/claim timing capability for every durable single-writer lane this
56    /// runtime claims: session execution leases, turn-input and queued-work
57    /// claims, and process leases. Effect-replay backends accept the same type
58    /// at construction. Defaults to [`LeaseTimings::default`] (30s TTL / 10s
59    /// renew).
60    pub lease_timings: crate::LeaseTimings,
61}
62
63#[derive(Clone)]
64pub struct RuntimeTracingConfig {
65    pub trace_sink: Option<Arc<dyn TraceSink>>,
66    pub trace_level: TraceLevel,
67    pub trace_context: TraceContext,
68}
69
70impl RuntimeHostConfig {
71    /// Construct a config with the three host-owned dependencies named
72    /// explicitly.
73    ///
74    /// There is intentionally no `Default`. The effect host and stores decide
75    /// a runtime's durability, so hosts must choose them rather than silently
76    /// inheriting in-memory implementations. Use [`RuntimeHostConfig::in_memory`]
77    /// to opt into the in-process / in-memory versions by name.
78    pub fn new(
79        effect_host: Arc<dyn EffectHost>,
80        attachment_store: Arc<dyn crate::AttachmentStore>,
81        process_env_store: Arc<dyn ProcessExecutionEnvStore>,
82    ) -> Self {
83        Self {
84            durability: RuntimeDurabilityConfig {
85                attachment_store: Arc::new(crate::SessionAttachmentStore::ephemeral(
86                    attachment_store,
87                )),
88                process_env_store,
89            },
90            process_engines: ProcessEngineRegistry::new(),
91            providers: RuntimeProviderConfig {
92                provider_resolver: Arc::new(crate::EmptyProviderResolver),
93            },
94            prompt: RuntimePromptConfig {
95                prompt: crate::PromptLayer::new(),
96            },
97            control: RuntimeControlConfig {
98                termination: TerminationPolicy::default(),
99                effect_host,
100                process_cancel_ability: Arc::new(crate::DefaultProcessCancelAbility),
101                lease_timings: crate::LeaseTimings::default(),
102            },
103            tracing: RuntimeTracingConfig {
104                trace_sink: None,
105                trace_level: TraceLevel::Standard,
106                trace_context: TraceContext::default(),
107            },
108            clock: Arc::new(super::SystemClock),
109        }
110    }
111
112    /// Replace the runtime time source. Hosts that need deterministic replay or
113    /// test-driven time inject their own [`Clock`](super::Clock); the default is
114    /// [`SystemClock`](super::SystemClock).
115    pub fn with_clock(mut self, clock: Arc<dyn super::Clock>) -> Self {
116        self.clock = clock;
117        self
118    }
119
120    /// Explicit in-process / in-memory configuration: an
121    /// [`InlineEffectHost`] and in-memory stores.
122    ///
123    /// Convenient for tests and local experiments; not durable. Named so the
124    /// choice is never silent.
125    pub fn in_memory() -> Self {
126        Self::new(
127            Arc::new(InlineEffectHost::default()),
128            Arc::new(crate::InMemoryAttachmentStore::new()),
129            Arc::new(InMemoryProcessExecutionEnvStore::new()),
130        )
131    }
132
133    pub fn with_process_env_store(
134        mut self,
135        process_env_store: Arc<dyn ProcessExecutionEnvStore>,
136    ) -> Self {
137        self.durability.process_env_store = process_env_store;
138        self
139    }
140
141    pub fn with_process_cancel_ability(
142        mut self,
143        process_cancel_ability: Arc<dyn crate::ProcessCancelAbility>,
144    ) -> Self {
145        self.control.process_cancel_ability = process_cancel_ability;
146        self
147    }
148
149    pub fn with_process_engine(mut self, engine: Arc<dyn crate::ProcessEngine>) -> Self {
150        self.process_engines = self.process_engines.with_engine(engine);
151        self
152    }
153
154    /// Replace the lease timing capability governing every durable lease and
155    /// claim this runtime takes.
156    pub fn with_lease_timings(mut self, lease_timings: crate::LeaseTimings) -> Self {
157        self.control.lease_timings = lease_timings;
158        self
159    }
160}
161
162/// Base host shape for embedded runtimes.
163#[derive(Clone)]
164pub struct EmbeddedRuntimeHost {
165    pub core: RuntimeHostConfig,
166    pub session_store_factory: Option<Arc<dyn SessionStoreFactory>>,
167    pub trigger_store: Option<Arc<dyn crate::TriggerStore>>,
168}
169
170impl EmbeddedRuntimeHost {
171    pub fn new(core: RuntimeHostConfig) -> Self {
172        let clock = Arc::clone(&core.clock);
173        Self {
174            core,
175            session_store_factory: None,
176            trigger_store: Some(Arc::new(crate::InMemoryTriggerStore::with_clock(clock))),
177        }
178    }
179
180    pub fn with_session_store_factory(
181        mut self,
182        session_store_factory: Arc<dyn SessionStoreFactory>,
183    ) -> Self {
184        self.session_store_factory = Some(session_store_factory);
185        self
186    }
187
188    pub fn with_trigger_store(mut self, store: Arc<dyn crate::TriggerStore>) -> Self {
189        self.trigger_store = Some(store);
190        self
191    }
192}
193
194/// Host shape for runtimes that support background plugin work.
195#[derive(Clone)]
196pub struct ProcessRuntimeHost {
197    pub embedded: EmbeddedRuntimeHost,
198    pub process_registry: Arc<dyn ProcessRegistry>,
199    pub process_work_driver: Option<ProcessWorkDriver>,
200    pub queued_work_driver: Option<QueuedWorkDriver>,
201}
202
203impl ProcessRuntimeHost {
204    pub fn new(embedded: EmbeddedRuntimeHost, process_registry: Arc<dyn ProcessRegistry>) -> Self {
205        Self {
206            embedded,
207            process_registry,
208            process_work_driver: None,
209            queued_work_driver: None,
210        }
211    }
212
213    pub fn with_process_work_driver(mut self, driver: ProcessWorkDriver) -> Self {
214        self.process_work_driver = Some(driver);
215        self
216    }
217
218    pub fn with_queued_work_driver(mut self, driver: QueuedWorkDriver) -> Self {
219        self.queued_work_driver = Some(driver);
220        self
221    }
222}
223
224#[derive(Clone)]
225pub(crate) struct RuntimeHost {
226    pub core: RuntimeHostConfig,
227    pub session_store_factory: Option<Arc<dyn SessionStoreFactory>>,
228    pub trigger_store: Option<Arc<dyn crate::TriggerStore>>,
229    pub process_registry: Option<Arc<dyn ProcessRegistry>>,
230    /// Host-owned process work driver. Absent when no process registry is wired.
231    pub process_work_driver: Option<ProcessWorkDriver>,
232    /// Host-owned queued work driver. Absent when queued work is delegated to an
233    /// external host or no session store exists.
234    pub queued_work_driver: Option<QueuedWorkDriver>,
235}
236
237impl RuntimeHost {
238    pub(crate) fn resolve_session_policy(
239        &self,
240        session_id: &str,
241        policy: crate::SessionPolicy,
242    ) -> Result<crate::RuntimeSessionPolicy, crate::SessionError> {
243        let provider_id = policy.recorded_provider_id();
244        let mut binding = self
245            .core
246            .providers
247            .provider_resolver
248            .resolve_provider_binding(provider_id)
249            .map_err(|err| match err {
250                crate::ProviderResolutionError::MissingProviderId => {
251                    crate::SessionError::ProviderUnconfigured {
252                        session_id: session_id.to_string(),
253                    }
254                }
255                crate::ProviderResolutionError::UnknownProvider { provider_id } => {
256                    crate::SessionError::ProviderUnavailable {
257                        provider_id,
258                        session_id: session_id.to_string(),
259                    }
260                }
261                crate::ProviderResolutionError::ProviderIdMismatch { expected, actual } => {
262                    crate::SessionError::ProviderMismatch {
263                        expected,
264                        actual,
265                        session_id: session_id.to_string(),
266                    }
267                }
268            })?;
269        binding.provider = binding.provider.with_clock(Arc::clone(&self.core.clock));
270        Ok(crate::RuntimeSessionPolicy::new(policy, binding))
271    }
272}
273
274impl From<EmbeddedRuntimeHost> for RuntimeHost {
275    fn from(value: EmbeddedRuntimeHost) -> Self {
276        Self {
277            core: value.core,
278            session_store_factory: value.session_store_factory,
279            trigger_store: value.trigger_store,
280            process_registry: None,
281            process_work_driver: None,
282            queued_work_driver: None,
283        }
284    }
285}
286
287impl From<ProcessRuntimeHost> for RuntimeHost {
288    fn from(value: ProcessRuntimeHost) -> Self {
289        Self {
290            core: value.embedded.core,
291            session_store_factory: value.embedded.session_store_factory,
292            trigger_store: value.embedded.trigger_store,
293            process_registry: Some(value.process_registry),
294            process_work_driver: value.process_work_driver,
295            queued_work_driver: value.queued_work_driver,
296        }
297    }
298}