Skip to main content

lash_core/runtime/
host.rs

1use std::sync::Arc;
2use std::sync::Mutex;
3
4use lash_trace::{TraceContext, TraceLevel, TraceSink};
5
6use super::process::ProcessRegistry;
7use super::{
8    EffectHost, InlineEffectHost, ProcessWorkPoke, QueuedWorkPoke, SessionStoreFactory,
9    TerminationPolicy,
10};
11
12/// Required host configuration for all runtimes.
13#[derive(Clone)]
14pub struct RuntimeHostConfig {
15    pub durability: RuntimeDurabilityConfig,
16    pub providers: RuntimeProviderConfig,
17    pub prompt: RuntimePromptConfig,
18    pub control: RuntimeControlConfig,
19    pub tracing: RuntimeTracingConfig,
20}
21
22#[derive(Clone)]
23pub struct RuntimeDurabilityConfig {
24    pub attachment_store: Arc<dyn crate::AttachmentStore>,
25    pub lashlang_artifact_store: Arc<dyn lashlang::LashlangArtifactStore>,
26    pub lashlang_process_cache: Arc<Mutex<lashlang::CompiledProcessCache>>,
27}
28
29#[derive(Clone)]
30pub struct RuntimeProviderConfig {
31    pub provider_resolver: Arc<dyn crate::RuntimeProviderResolver>,
32}
33
34#[derive(Clone)]
35pub struct RuntimePromptConfig {
36    pub prompt: crate::PromptLayer,
37}
38
39#[derive(Clone)]
40pub struct RuntimeControlConfig {
41    pub effect_host: Arc<dyn EffectHost>,
42    pub process_cancel_ability: Arc<dyn crate::ProcessCancelAbility>,
43    pub termination: TerminationPolicy,
44}
45
46#[derive(Clone)]
47pub struct RuntimeTracingConfig {
48    pub trace_sink: Option<Arc<dyn TraceSink>>,
49    pub lashlang_execution_sink: Option<Arc<dyn TraceSink>>,
50    pub trace_level: TraceLevel,
51    pub trace_context: TraceContext,
52}
53
54impl RuntimeHostConfig {
55    /// Construct a config with the three host-owned dependencies named
56    /// explicitly.
57    ///
58    /// There is intentionally no `Default`. The effect host, Lashlang
59    /// artifact store, and attachment store decide a runtime's durability, so
60    /// hosts must choose them rather than silently inheriting in-memory
61    /// implementations. Use [`RuntimeHostConfig::in_memory`] to opt into the
62    /// in-process / in-memory versions by name.
63    pub fn new(
64        effect_host: Arc<dyn EffectHost>,
65        lashlang_artifact_store: Arc<dyn lashlang::LashlangArtifactStore>,
66        attachment_store: Arc<dyn crate::AttachmentStore>,
67    ) -> Self {
68        Self {
69            durability: RuntimeDurabilityConfig {
70                attachment_store,
71                lashlang_artifact_store,
72                lashlang_process_cache: Arc::new(Mutex::new(lashlang::CompiledProcessCache::new())),
73            },
74            providers: RuntimeProviderConfig {
75                provider_resolver: Arc::new(crate::EmptyProviderResolver),
76            },
77            prompt: RuntimePromptConfig {
78                prompt: crate::PromptLayer::new(),
79            },
80            control: RuntimeControlConfig {
81                termination: TerminationPolicy::default(),
82                effect_host,
83                process_cancel_ability: Arc::new(crate::DefaultProcessCancelAbility),
84            },
85            tracing: RuntimeTracingConfig {
86                trace_sink: None,
87                lashlang_execution_sink: None,
88                trace_level: TraceLevel::Standard,
89                trace_context: TraceContext::default(),
90            },
91        }
92    }
93
94    /// Explicit in-process / in-memory configuration: an
95    /// [`InlineEffectHost`], the process-global in-memory Lashlang
96    /// artifact store, and an in-memory attachment store.
97    ///
98    /// Convenient for tests and local experiments; not durable. Named so the
99    /// choice is never silent.
100    pub fn in_memory() -> Self {
101        Self::new(
102            Arc::new(InlineEffectHost::default()),
103            lashlang::global_in_memory_lashlang_artifact_store(),
104            Arc::new(crate::InMemoryAttachmentStore::new()),
105        )
106    }
107
108    pub fn with_process_cancel_ability(
109        mut self,
110        process_cancel_ability: Arc<dyn crate::ProcessCancelAbility>,
111    ) -> Self {
112        self.control.process_cancel_ability = process_cancel_ability;
113        self
114    }
115}
116
117/// Base host shape for embedded runtimes.
118#[derive(Clone)]
119pub struct EmbeddedRuntimeHost {
120    pub core: RuntimeHostConfig,
121    pub session_store_factory: Option<Arc<dyn SessionStoreFactory>>,
122    pub trigger_store: Option<Arc<dyn crate::TriggerStore>>,
123}
124
125impl EmbeddedRuntimeHost {
126    pub fn new(core: RuntimeHostConfig) -> Self {
127        Self {
128            core,
129            session_store_factory: None,
130            trigger_store: Some(Arc::new(crate::InMemoryTriggerStore::default())),
131        }
132    }
133
134    pub fn with_session_store_factory(
135        mut self,
136        session_store_factory: Arc<dyn SessionStoreFactory>,
137    ) -> Self {
138        self.session_store_factory = Some(session_store_factory);
139        self
140    }
141
142    pub fn with_trigger_store(mut self, store: Arc<dyn crate::TriggerStore>) -> Self {
143        self.trigger_store = Some(store);
144        self
145    }
146}
147
148/// Host shape for runtimes that support background plugin work.
149#[derive(Clone)]
150pub struct ProcessRuntimeHost {
151    pub embedded: EmbeddedRuntimeHost,
152    pub process_registry: Arc<dyn ProcessRegistry>,
153}
154
155impl ProcessRuntimeHost {
156    pub fn new(embedded: EmbeddedRuntimeHost, process_registry: Arc<dyn ProcessRegistry>) -> Self {
157        Self {
158            embedded,
159            process_registry,
160        }
161    }
162}
163
164#[derive(Clone)]
165pub(crate) struct RuntimeHost {
166    pub core: RuntimeHostConfig,
167    pub session_store_factory: Option<Arc<dyn SessionStoreFactory>>,
168    pub trigger_store: Option<Arc<dyn crate::TriggerStore>>,
169    pub process_registry: Option<Arc<dyn ProcessRegistry>>,
170    /// Wakes the host's [`ProcessWorkRunner`](super::ProcessWorkRunner) so a
171    /// successful process start is consumed promptly. Absent when no work runner
172    /// is wired (e.g. a registry-less host); poking is then a no-op.
173    pub process_work_poke: Option<ProcessWorkPoke>,
174    /// Wakes the host's [`QueuedWorkRunner`](super::QueuedWorkRunner) so queued
175    /// turn work drains promptly after queue ingress.
176    pub queued_work_poke: Option<QueuedWorkPoke>,
177}
178
179impl RuntimeHost {
180    pub(crate) fn resolve_session_policy(
181        &self,
182        session_id: &str,
183        policy: crate::SessionPolicy,
184    ) -> Result<crate::RuntimeSessionPolicy, crate::SessionError> {
185        let provider_id = policy.recorded_provider_id();
186        let binding = self
187            .core
188            .providers
189            .provider_resolver
190            .resolve_provider_binding(provider_id)
191            .map_err(|err| match err {
192                crate::ProviderResolutionError::MissingProviderId => {
193                    crate::SessionError::ProviderUnconfigured {
194                        session_id: session_id.to_string(),
195                    }
196                }
197                crate::ProviderResolutionError::UnknownProvider { provider_id } => {
198                    crate::SessionError::ProviderUnavailable {
199                        provider_id,
200                        session_id: session_id.to_string(),
201                    }
202                }
203                crate::ProviderResolutionError::ProviderIdMismatch { expected, actual } => {
204                    crate::SessionError::ProviderMismatch {
205                        expected,
206                        actual,
207                        session_id: session_id.to_string(),
208                    }
209                }
210            })?;
211        Ok(crate::RuntimeSessionPolicy::new(policy, binding))
212    }
213}
214
215impl From<EmbeddedRuntimeHost> for RuntimeHost {
216    fn from(value: EmbeddedRuntimeHost) -> Self {
217        Self {
218            core: value.core,
219            session_store_factory: value.session_store_factory,
220            trigger_store: value.trigger_store,
221            process_registry: None,
222            process_work_poke: None,
223            queued_work_poke: None,
224        }
225    }
226}
227
228impl From<ProcessRuntimeHost> for RuntimeHost {
229    fn from(value: ProcessRuntimeHost) -> Self {
230        Self {
231            core: value.embedded.core,
232            session_store_factory: value.embedded.session_store_factory,
233            trigger_store: value.embedded.trigger_store,
234            process_registry: Some(value.process_registry),
235            process_work_poke: None,
236            queued_work_poke: None,
237        }
238    }
239}