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