lash_core/runtime/
environment.rs1use std::sync::Arc;
28
29use lash_trace::{TraceContext, TraceLevel, TraceSink};
30
31#[cfg(test)]
32use super::InlineEffectHost;
33use super::process::ProcessRegistry;
34use super::{EffectHost, RuntimeHostConfig, TerminationPolicy};
35
36#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
45pub enum Residency {
46 #[default]
49 KeepAll,
50 ActivePathOnly,
56}
57
58#[derive(Clone)]
66pub struct RuntimeEnvironment {
67 pub plugin_host: Option<Arc<crate::PluginHost>>,
70
71 pub residency: Residency,
77
78 pub process_registry: Option<Arc<dyn ProcessRegistry>>,
80
81 pub trigger_store: Option<Arc<dyn crate::TriggerStore>>,
83
84 pub session_store_factory: Option<Arc<dyn crate::SessionStoreFactory>>,
87
88 pub process_work_driver: Option<super::ProcessWorkDriver>,
91
92 pub queued_work_driver: Option<super::QueuedWorkDriver>,
96
97 pub core: RuntimeHostConfig,
98}
99
100impl RuntimeEnvironment {
101 pub fn builder() -> RuntimeEnvironmentBuilder {
102 RuntimeEnvironmentBuilder::default()
103 }
104}
105
106pub struct ParkedSession {
111 pub(crate) session_id: String,
112 pub(crate) store: Arc<dyn crate::store::RuntimePersistence>,
113 pub(crate) policy: crate::SessionPolicy,
114}
115
116impl ParkedSession {
117 pub fn session_id(&self) -> &str {
118 &self.session_id
119 }
120}
121
122pub struct RuntimeEnvironmentBuilder {
124 env: RuntimeEnvironment,
125}
126
127impl Default for RuntimeEnvironmentBuilder {
128 fn default() -> Self {
129 Self {
134 env: RuntimeEnvironment {
135 plugin_host: None,
136 residency: Residency::default(),
137 process_registry: None,
138 trigger_store: Some(Arc::new(crate::InMemoryTriggerStore::default())),
139 session_store_factory: None,
140 process_work_driver: None,
141 queued_work_driver: None,
142 core: RuntimeHostConfig::in_memory(),
143 },
144 }
145 }
146}
147
148impl RuntimeEnvironmentBuilder {
149 pub fn with_plugin_host(mut self, host: Arc<crate::PluginHost>) -> Self {
150 self.env.plugin_host = Some(host);
151 self
152 }
153
154 pub fn with_residency(mut self, residency: Residency) -> Self {
155 self.env.residency = residency;
156 self
157 }
158
159 pub fn with_process_registry(mut self, process_registry: Arc<dyn ProcessRegistry>) -> Self {
160 self.env.process_registry = Some(process_registry);
161 self
162 }
163
164 pub fn with_trigger_store(mut self, store: Arc<dyn crate::TriggerStore>) -> Self {
165 self.env.trigger_store = Some(store);
166 self
167 }
168
169 pub fn with_session_store_factory(
170 mut self,
171 factory: Arc<dyn crate::SessionStoreFactory>,
172 ) -> Self {
173 self.env.session_store_factory = Some(factory);
174 self
175 }
176
177 pub fn with_process_work_driver(mut self, driver: super::ProcessWorkDriver) -> Self {
180 self.env.process_work_driver = Some(driver);
181 self
182 }
183
184 pub fn with_queued_work_driver(mut self, driver: super::QueuedWorkDriver) -> Self {
185 self.env.queued_work_driver = Some(driver);
186 self
187 }
188
189 pub fn with_runtime_host_config(mut self, core: RuntimeHostConfig) -> Self {
190 self.env.core = core;
191 self
192 }
193
194 pub fn with_attachment_store(mut self, store: Arc<dyn crate::AttachmentStore>) -> Self {
195 self.env.core.durability.attachment_store =
196 Arc::new(crate::SessionAttachmentStore::ephemeral(store));
197 self
198 }
199
200 pub fn with_prompt_template(mut self, template: crate::PromptTemplate) -> Self {
201 self.env.core.prompt.prompt.template = Some(template);
202 self
203 }
204
205 pub fn with_prompt_contribution(mut self, contribution: crate::PromptContribution) -> Self {
206 self.env.core.prompt.prompt.add_contribution(contribution);
207 self
208 }
209
210 pub fn with_replaced_prompt_slot(
211 mut self,
212 slot: crate::PromptSlot,
213 contributions: impl IntoIterator<Item = crate::PromptContribution>,
214 ) -> Self {
215 self.env
216 .core
217 .prompt
218 .prompt
219 .replace_slot(slot, contributions);
220 self
221 }
222
223 pub fn with_cleared_prompt_slot(mut self, slot: crate::PromptSlot) -> Self {
224 self.env.core.prompt.prompt.clear_slot(slot);
225 self
226 }
227
228 pub fn with_prompt_layer(mut self, prompt: crate::PromptLayer) -> Self {
229 self.env.core.prompt.prompt = prompt;
230 self
231 }
232
233 pub fn with_trace_sink(mut self, sink: Option<Arc<dyn TraceSink>>) -> Self {
234 self.env.core.tracing.trace_sink = sink;
235 self
236 }
237
238 pub fn with_trace_level(mut self, level: TraceLevel) -> Self {
239 self.env.core.tracing.trace_level = level;
240 self
241 }
242
243 pub fn with_trace_context(mut self, context: TraceContext) -> Self {
244 self.env.core.tracing.trace_context = context;
245 self
246 }
247
248 pub fn with_termination(mut self, termination: TerminationPolicy) -> Self {
249 self.env.core.control.termination = termination;
250 self
251 }
252
253 pub fn with_effect_host(mut self, effect_host: Arc<dyn EffectHost>) -> Self {
254 self.env.core.control.effect_host = effect_host;
255 self
256 }
257
258 pub fn with_provider_resolver(
259 mut self,
260 provider_resolver: Arc<dyn crate::RuntimeProviderResolver>,
261 ) -> Self {
262 self.env.core.providers.provider_resolver = provider_resolver;
263 self
264 }
265
266 pub fn build(self) -> RuntimeEnvironment {
267 self.env
268 }
269}
270
271#[cfg(test)]
272mod tests {
273 use super::*;
274
275 #[test]
276 fn builder_methods_configure_runtime_host() {
277 let attachment_store: Arc<dyn crate::AttachmentStore> =
278 Arc::new(crate::InMemoryAttachmentStore::new());
279 let effect_host: Arc<dyn EffectHost> = Arc::new(InlineEffectHost::default());
280 let trace_context = TraceContext::default().for_session("session-1");
281 let termination = TerminationPolicy {
282 treat_missing_done_as_failure: false,
283 };
284
285 let env = RuntimeEnvironment::builder()
286 .with_attachment_store(Arc::clone(&attachment_store))
287 .with_prompt_template(crate::default_prompt_template())
288 .with_trace_sink(Some(Arc::new(lash_trace::JsonlTraceSink::new(
289 std::env::temp_dir().join("lash-runtime-environment-builder-test.jsonl"),
290 ))))
291 .with_trace_level(TraceLevel::Extended)
292 .with_trace_context(trace_context.clone())
293 .with_termination(termination.clone())
294 .with_effect_host(Arc::clone(&effect_host))
295 .build();
296
297 assert!(Arc::ptr_eq(
298 env.core.durability.attachment_store.backend(),
299 &attachment_store
300 ));
301 assert!(env.core.prompt.prompt.template.is_some());
302 assert!(env.core.tracing.trace_sink.is_some());
303 assert_eq!(env.core.tracing.trace_level, TraceLevel::Extended);
304 assert_eq!(env.core.tracing.trace_context, trace_context);
305 assert_eq!(
306 env.core.control.termination.treat_missing_done_as_failure,
307 termination.treat_missing_done_as_failure
308 );
309 assert!(Arc::ptr_eq(&env.core.control.effect_host, &effect_host));
310 }
311
312 #[test]
313 fn runtime_host_config_replaces_core_config() {
314 let mut core = RuntimeHostConfig::in_memory();
315 core.tracing.trace_level = TraceLevel::Extended;
316 core.control.termination = TerminationPolicy {
317 treat_missing_done_as_failure: false,
318 };
319
320 let env = RuntimeEnvironment::builder()
321 .with_trace_level(TraceLevel::Standard)
322 .with_runtime_host_config(core)
323 .build();
324
325 assert_eq!(env.core.tracing.trace_level, TraceLevel::Extended);
326 assert!(!env.core.control.termination.treat_missing_done_as_failure);
327 }
328
329 #[test]
330 fn runtime_environment_does_not_mirror_runtime_host_config_fields() {
331 let source = std::fs::read_to_string(
332 std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("src/runtime/environment.rs"),
333 )
334 .expect("read environment source");
335 for field in [
336 ["pub ", "attachment_store:"].concat(),
337 ["pub ", "prompt:"].concat(),
338 ["pub ", "trace_sink:"].concat(),
339 ["pub ", "trace_level:"].concat(),
340 ["pub ", "trace_context:"].concat(),
341 ["pub ", "termination:"].concat(),
342 ["pub ", "effect_host:"].concat(),
343 ["mirror ", "`RuntimeHostConfig`"].concat(),
344 ] {
345 assert!(
346 !source.contains(&field),
347 "found mirrored field/comment: {field}"
348 );
349 }
350 }
351}