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