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